Skip to content

fix(cloudflare/containers): wrangler-parity defaults and readiness for fast, reliable starts - #708

Merged
sam-goodwin merged 4 commits into
mainfrom
sam/container-perf
Jun 29, 2026
Merged

fix(cloudflare/containers): wrangler-parity defaults and readiness for fast, reliable starts#708
sam-goodwin merged 4 commits into
mainfrom
sam/container-perf

Conversation

@sam-goodwin

Copy link
Copy Markdown
Contributor

Cloudflare Containers started far slower and far less reliably than the same image deployed with wrangler + @cloudflare/containers. At 100 concurrent cold starts Alchemy landed ~2/100 (mass TimeoutErrors); wrangler did 100/100 in ~40s. This fixes both root causes so an Alchemy container behaves like a wrangler deployd one out of the box.

Root cause 1 — defaults

maxInstances defaulted to 1, which serialized every Durable Object instance through a single container slot. Now matches wrangler's real defaults (from wrangler-dist/cli.js):

- instances:    news.instances    ?? 1
- maxInstances: news.maxInstances ?? 1
+ instances:    news.instances    ?? 0      // scale-from-zero (wrangler forces 0 when max is set)
+ maxInstances: news.maxInstances ?? 20     // wrangler: `container.max_instances ?? 20`
  // instance_type is mutually exclusive with vcpu/memory/disk, so only default when none are set
- instanceType: props.instanceType
+ instanceType: props.instanceType ?? (noCustomLimits ? "lite" : undefined)

Root cause 2 — readiness polling

StartContainer used a coarse exponential backoff (1–3s, recurs(75) ≈ 225s budget) with no per-probe timeout, so a probe to a not-yet-listening port could hang for minutes. Rewritten to mirror @cloudflare/containers startAndWaitForPorts exactly:

Result (N=100, concurrency=100, identical oven/bun:latest image)

before after wrangler
started ~2/100 100/100 100/100
p50 49–128s ~37s ~33s

Also included

  • test/Cloudflare/Container/Container.benchmark.test.ts (+ fixtures/benchmark/) — cold-start benchmark across effectful / external / remote-image variants.
  • perf/cloudflare-containers-wrangler/ — a zero-Alchemy wrangler + @cloudflare/containers control harness used to establish the parity baseline. Added perf/* to workspaces and the root tsconfig references.

sam-goodwin and others added 2 commits June 29, 2026 15:39
…r fast, reliable starts

Cloudflare Containers were dramatically slower and less reliable than the
same image deployed with `wrangler` + `@cloudflare/containers`. Two root
causes, both fixed here so containers behave like a `wrangler deploy`d one
out of the box:

- Defaults: `maxInstances` defaulted to 1, serializing every Durable Object
  instance through a single container slot (the dominant cause of timeouts
  under load). Match wrangler's defaults instead: `maxInstances: 20`,
  `instances: 0` (scale-from-zero), `instanceType: "lite"` (when no explicit
  vcpu/memory/disk).
- Readiness: replace the coarse exponential backoff (1-3s, ~225s budget that
  could hang for minutes) in StartContainer with the exact shape of
  `@cloudflare/containers` `startAndWaitForPorts`: 300ms fixed poll, 5s
  per-probe cap, bounded 8s+20s phases, start coalescing via a semaphore
  (their `startInFlight`, cloudflare/containers#173), and typed errors
  (NoContainerInstance/RateLimited/Crashed) with rate-limit backoff instead
  of hammering the allocator.

After: 100/100 instances start across effectful/external/remote variants at
N=100 concurrency, p50 ~37s vs wrangler ~33s (was ~2/100 with mass timeouts).

Adds an in-repo cold-start benchmark (test/Cloudflare/Container/
Container.benchmark.test.ts + fixtures) and a zero-Alchemy wrangler control
harness under perf/cloudflare-containers-wrangler to keep the comparison
honest.

Co-authored-by: Cursor <cursoragent@cursor.com>
…fixtures

Move the wrangler + @cloudflare/containers control harness from the standalone
perf/ package into the alchemy test fixtures alongside the benchmark it
baselines:

  perf/cloudflare-containers-wrangler/{src/index.ts,bench.ts,Dockerfile,wrangler.jsonc}
  -> packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/

- `@cloudflare/containers` (bumped to ^0.3.7) and `wrangler` (^4.103.0) now live
  in the root package.json catalog and are referenced as `catalog:` devDeps of
  the alchemy package, so the harness resolves them without its own install.
- Excluded the wrangler subdir from the alchemy test tsconfig: it imports
  `cloudflare:workers` (via @cloudflare/containers) and uses Workers runtime
  globals that only resolve under wrangler's bundler, and it is deployed/run on
  its own (never imported by the vitest suite).
- Reverted the now-unneeded perf/* workspace + tsconfig references.

Run from the new dir: `bun x wrangler deploy` then
`WORKER_URL=… bun bench.ts`.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alchemy-version-bot

alchemy-version-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/4515b3a

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/4515b3a

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/4515b3a

sam-goodwin and others added 2 commits June 29, 2026 15:58
…ot per call

The start mutex and confirmed-ready port cache were created in each
`startContainer` closure. The `layer` path calls `startContainer` once per
Durable Object so its single stub coalesced correctly, but a caller invoking
`startContainer` directly from multiple places got independent mutexes — two
could both observe `running === false` and both call `container.start()`, with
the second throwing "already running" (cloudflare/containers#173).

Move the mutex + ready-port cache into a module-scoped WeakMap keyed by the
DO's `DurableObjectState` (stable and unique per instance, resolved via
`Effect.serviceOption` so it adds no hard requirement). All `startContainer`
calls for the same instance now share one mutex/cache; different
`getByName(...)` instances stay independent (we intentionally do not key on the
shared logical id).

Co-authored-by: Cursor <cursoragent@cursor.com>
…for start coordination

A container only ever runs inside a Durable Object, so depending on
`DurableObjectState` is an honest, correct requirement — not something to
resolve optionally. Replace `Effect.serviceOption(...)` + per-call fallback
(which silently degraded back to per-call coordination if state was absent)
with a plain `yield* DurableObjectState`. The `layer` helper already casts away
the requirement from its public type; direct callers correctly surface it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sam-goodwin
sam-goodwin merged commit c740f0f into main Jun 29, 2026
9 checks passed
@sam-goodwin
sam-goodwin deleted the sam/container-perf branch June 29, 2026 23:20
DavidJFelix pushed a commit to DavidJFelix/alchemy-effect that referenced this pull request Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant