1# cloudflare-preview-benchmark
2
3The app behind the PR-lifecycle animation on the alchemy.run landing page: an R2 bucket (`Photos`), a KV namespace (`Sessions`), and an Effect-native Worker (`Api`) bound to both. It is deployed per pull request as a preview stage, tested over HTTP, and destroyed.
4
5| Route | Behavior |
6| --- | --- |
7| `GET /photos` | JSON gallery listing every object key in `Photos` |
8| `PUT /photos/:key` | Uploads the request body to `Photos` |
9| `GET /session` | Starts a session and stores it in `Sessions`; with an `x-session-id` header, reads it back |
10
11This example creates billable Cloudflare infrastructure. Destroy it when you are done.
12
13## Run it
14
15```sh
16bun alchemy deploy --stage pr-147
17STAGE=pr-147 bun test
18bun alchemy destroy --stage pr-147
19```
20
21`test/preview.test.ts` deploys through the Alchemy test harness in `beforeAll`. That deploy is a no-op when the stage already exists. Without `STAGE`, the harness uses its default stage, `test_$USER`. The stack is destroyed after the tests only when `CI` is set.
22
23`Photos` sets `forceDestroy: true` because the upload test leaves an object in the bucket, and R2 refuses to delete a non-empty bucket.
24
25## Benchmark
26
27The "full picture" animation on the landing page (`website/src/components/marketing-islands/PRLifecycle.tsx`) replays these timings in real time for the deploy, test and destroy logs. Re-run the benchmark when Alchemy gets faster or slower, and update the `T_*` constants at the top of that file to match.
28
29### Run it
30
31You need a Cloudflare account and an Alchemy profile with credentials for it (`bunx alchemy login --profile <name>` creates one).
32
33```sh
34# from the repo root, once
35pnpm install
36
37cd examples/cloudflare-preview-benchmark
38ALCHEMY_PROFILE=<name> bun run bench
39```
40
41`bun run bench` runs `scripts/bench.sh`, which times one full preview lifecycle and prints one line per phase:
42
43```
44deploy 11.62s
45url live 0.41s https://api-….workers.dev
46test 4.53s
47destroy 3.80s
48total 20.36s (logs: .alchemy/bench/<timestamp>-*.log)
49```
50
51- `STAGE` picks the stage name (default `pr-147`). Use a unique one if several people share an account.
52- Each phase's full CLI output is written to `.alchemy/bench/`. The per-resource breakdown below comes from the deploy and destroy logs.
53- The script always destroys the stage at the end. If it fails midway, clean up with `bunx alchemy destroy --stage <stage>`.
54- Run it three times and take the median; the first run after an install is slower while Bun warms its cache.
55
56### Results
57
58Median of three full cycles, wall clock:
59
60| Phase | Time |
61| --- | --- |
62| **Deploy (create 3 resources)** | **11.6s** |
63| · CLI startup, stack import, state load | 1.2s |
64| · plan | 1.3s |
65| · `Sessions` created | 0.7s |
66| · `Photos` created | 3.6s |
67| · `Api` created (waits for `Photos`, bundles 2.2s, uploads and enables workers.dev 3.3s) | 9.4s |
68| **Worker URL returns 200 after deploy** | **+0.4s** |
69| **Tests, plain `fetch` against the URL** | **0.7s** |
70| **Tests, through the Alchemy harness (`bun test`)** | **4.5s** |
71| · GET /photos renders the gallery | 0.24s |
72| · uploads a photo to R2 | 0.28s |
73| · session survives a reload | 0.26s |
74| **Deploy with no changes** | **4.0s** |
75| **Deploy after a one-line Worker change** | **7.6s** |
76| · new code served at the URL | +0.2s |
77| **Destroy** | **3.8s** |
78| · `Api` deleted | 0.7s |
79| · `Photos` emptied and deleted | 1.4s |
80| · `Sessions` deleted | 0.4s |
81
82Most of the harness test time is the no-op deploy in `beforeAll`: it re-reads every resource from Cloudflare (about 3.2s). The same three tests take 0.7s without it. Running the CLI under Node instead of Bun added about 0.7s per command.
83
84These numbers come from one Linux machine and one Cloudflare account, using alchemy 2.0.0-beta.79, effect 4.0.0-rc.117 and Bun 1.3.13. Treat them as indicative.
85
86The landing-page animation also shows a `GitHub.Comment` posting the preview URL. It is not part of this example or the benchmark because it needs a GitHub token and a target PR.