fix(cloudflare/containers): wrangler-parity defaults and readiness for fast, reliable starts - #708
Merged
Merged
Conversation
…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>
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 |
…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>
DavidJFelix
pushed a commit
to DavidJFelix/alchemy-effect
that referenced
this pull request
Aug 7, 2026
…r fast, reliable starts (alchemy-run#708)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (massTimeoutErrors); wrangler did 100/100 in ~40s. This fixes both root causes so an Alchemy container behaves like awrangler deployd one out of the box.Root cause 1 — defaults
maxInstancesdefaulted to1, which serialized every Durable Object instance through a single container slot. Now matches wrangler's real defaults (fromwrangler-dist/cli.js):Root cause 2 — readiness polling
StartContainerused 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/containersstartAndWaitForPortsexactly:300msfixed poll (INSTANCE_POLL_INTERVAL_MS),5sper-probe cap (PING_TIMEOUT_MS)8s+20sphases (TIMEOUT_TO_GET_CONTAINER_MS+TIMEOUT_TO_GET_PORTS_MS)startInFlight, Race condition causes container.fetch to throw "start() cannot be called on a container that is already running." cloudflare/containers#173)NoContainerInstanceError/ContainerRateLimitedError/ContainerCrashedError, with rate-limit backoff instead of hammering the allocatorResult (N=100, concurrency=100, identical
oven/bun:latestimage)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-Alchemywrangler+@cloudflare/containerscontrol harness used to establish the parity baseline. Addedperf/*to workspaces and the root tsconfig references.