2.0.0-beta.61 - Workers Cache, Routes & Sync
beta.61 adds Workers Cache with an Effect-native
ExecutionContext, Wrangler-style zone routes, alchemy sync for repairing out-of-band drift, a much fuller
Workflows API (retries, rollback, events), resource type
aliases that heal the beta.59 renames, and a long list of
reliability fixes.
Workers Cache
Section titled “Workers Cache”Workers Cache —
Cloudflare’s regionally tiered cache in front of Worker
entrypoints — lands as both a binding and a prop
(#752).
Effect-native Workers enable it with Cloudflare.cache(), which
also returns the typed purge client:
Effect.gen(function* () { // init: enables Workers Cache on this Worker at deploy time const { purge } = yield* Cloudflare.cache({ crossVersionCache: true });
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; if (request.url.startsWith("/invalidate")) { yield* purge({ tags: ["products"] }); // typed CachePurgeError return HttpServerResponse.text("purged"); } return HttpServerResponse.text("hello", { headers: { "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", "Cache-Tag": "products", }, }); }), };})Async Workers use the prop:
const worker = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", cache: { enabled: true, crossVersionCache: true },});Effect-native ExecutionContext
Section titled “Effect-native ExecutionContext”WorkerExecutionContext used to hand you the raw
cf.ExecutionContext. It’s now an Effect wrapper — and like
DurableObjectState, it can be yielded from the Worker’s init
closure (or any Layer); its methods resolve the live per-event
context:
Effect.gen(function* () { const exec = yield* Cloudflare.WorkerExecutionContext; // init
return { fetch: Effect.gen(function* () { // respond now, finish work in the background yield* exec.waitUntil(journal.record(entry).pipe(Effect.delay("5 seconds"))); return HttpServerResponse.text("ok"); }), };})Migrating is mechanical — ctx.waitUntil(promise) becomes
yield* exec.waitUntil(effect).
Zone Routes on Workers
Section titled “Zone Routes on Workers”Cloudflare.Worker accepts Wrangler-style zone routes via a new
routes prop
(#438).
Thanks utopy!
yield* Cloudflare.Worker("Api", { main: import.meta.filename, routes: [ { pattern: "api.example.com/*", zoneName: "example.com" }, { pattern: "example.com/api/*", zoneId: "<YOUR_ZONE_ID>" }, ],});Each entry takes zoneName / zoneId (the Wrangler equivalents)
or a zone reference — and when the zone is omitted, it’s inferred
from the pattern hostname. Routes are reconciled on deploy and
cleaned up on destroy.
alchemy sync
Section titled “alchemy sync”Cloud state drifts: someone edits a resource in the dashboard, a
bucket gets deleted, tags get mangled. alchemy sync converges it
back to the last-deployed state — without re-running your stack
program
(#766):
alchemy sync ./alchemy.run.ts --stage prod # detect + repairalchemy sync ./alchemy.run.ts --stage prod --dry-run # detect onlyPer resource it runs observe → compare → converge: read observes
the live cloud state, a deep-compare against the persisted
attributes decides unchanged vs drifted, and drift is repaired by
reconcile with the persisted props as the desired state. A
resource deleted out-of-band is recreated under the same
instance id, so deterministic physical names regenerate
identically. Resources sync concurrently, and failures are
aggregated after every resource has been attempted.
Workflows: retries, rollback, and events
Section titled “Workflows: retries, rollback, and events”The Effect-native Workflow wrapper now covers the full Workers API surface, 1:1 with the native binding (#611). Thanks Gerben Mulder!
create takes the native options object — the breaking change
above — which also unlocks id and retention:
const instance = yield* workflow.create({ orderId: "abc" });const instance = yield* workflow.create({ id: "order-abc", params: { orderId: "abc" }, retention: { successRetention: "1 day", errorRetention: "7 days" },});task gains retries, timeout, and rollback, and
WorkflowStepContext exposes the current attempt:
const result = yield* Cloudflare.Workflows.task( "call-api", Effect.gen(function* () { const context = yield* Cloudflare.Workflows.WorkflowStepContext; return { attempt: context.attempt }; }), { retries: { limit: 3, delay: "5 seconds", backoff: "linear" }, rollback: ({ output }) => (output ? cleanup(output.id) : Effect.void), },);waitForEvent parks the instance until a matching sendEvent:
// inside the workflowconst approval = yield* Cloudflare.Workflows.waitForEvent<{ approved: boolean }>( "approval", { type: "approval", timeout: "1 day" },);
// from outsideyield* instance.sendEvent({ type: "approval", payload: { approved: true } });createBatch, restart, rollback status, and extended event
metadata round out the surface. Docs:
Workflows.
Resource type aliases
Section titled “Resource type aliases”Renaming a resource’s type string used to strand state persisted under the old name — provider lookup died on the legacy type. Resources now declare their former names (#765):
export const Queue = Resource<Queue>("Cloudflare.Queues.Queue", { aliases: ["Cloudflare.Queue"],});Plan, apply, destroy, logs, and tail all resolve providers
through aliases, and a noop deploy migrates state rows to the
canonical name. All 74 resources renamed in the beta.59
namespace alignment are annotated with their pre-rename alias — so
state written by beta.58 or earlier now deploys, destroys, and
replaces cleanly instead of erroring on the legacy type.
AI Gateway BYOK: AI.ProviderKey
Section titled “AI Gateway BYOK: AI.ProviderKey”A bring-your-own-key provider on an AI Gateway needs two coordinated
resources — a Secrets Store Secret with an exact
{gatewayId}_{providerSlug}_{alias} name, and a GatewayProvider
that references it. Cloudflare.AI.ProviderKey owns that contract
as one resource
(#586).
Thanks Alex!
const { secret, gatewayProvider } = yield* Cloudflare.AI.ProviderKey("OpenAiKey", { store, gatewayId: gateway.gatewayId, providerSlug: "openai", value: yield* Config.redacted("OPENAI_API_KEY"),});Docs: AI Gateway.
Lambda async invocation config
Section titled “Lambda async invocation config”Lambda’s asynchronous invocation settings — retries, event age,
success/failure destinations — land as an eventInvokeConfig prop
on Function and Alias
(#627).
Thanks José Netto!
const fn = yield* AWS.Lambda.Function("AsyncFn", { main: "./src/handler.ts", eventInvokeConfig: { maximumRetryAttempts: 0, maximumEventAgeInSeconds: 60, destinationConfig: { OnFailure: { Destination: queue.queueArn }, }, },});R2 Bucket cors
Section titled “R2 Bucket cors”The cors prop that Cloudflare.R2.Bucket had in v1 is restored
(#771) —
public buckets serving browser range-reads (e.g. PMTiles) declare
CORS in the stack again instead of out-of-band:
const bucket = yield* Cloudflare.R2.Bucket("Tiles", { domains: [{ name: "tiles.example.com" }], cors: [ { allowedMethods: ["GET", "HEAD"], allowedOrigins: ["https://map.example.com"], allowedHeaders: ["range"], exposeHeaders: ["etag", "content-range"], maxAgeSeconds: 3600, }, ],});Rules use the flat S3-style shape and reconcile like
lifecycleRules — observed vs desired, so out-of-band drift and
adoption converge correctly.
Also in this release
Section titled “Also in this release”- Worker metadata-only changes now deploy (#747) — compatibility flags/date, observability, placement, limits, and binding changes are folded into the update diff via a metadata hash; previously they planned as noops and silently never shipped. The first deploy after upgrading plans a one-time update per Worker to backfill the hash. Thanks Alex!
- Wedged stacks recover
(#767,
#770) —
a deploy interrupted mid-create used to persist a state row whose
Output-valued props can’t round-trip, crashing every subsequent
plan/deploy/destroy. Every provider’sread/diffis audited so the engine re-drives the create and reconcile converges on the half-created resource. - Script uploads retry every binding-target-not-found error (#753) — KV, R2, D1, Queues, DO classes, Hyperdrive, Vectorize, and more, covering Cloudflare’s deploy-time propagation lag.
Resource.refvalues bind natively in Workerenv(#756) — a ref passed as an env binding used to degrade to a plain JSON env var and break at runtime; refs now classify exactly like locally-declared resources.- Multiple queue consumers on one Worker (#466) — event dispatch runs every listener for an event type instead of letting the first queue subscription swallow the rest. Thanks Leonardo E. Dominguez!
- State-store errors are actionable
(#737) —
30 days of production traces triaged; empty
StateStoreError:messages, opaque decode errors, and JSON-parse crashes now surface real messages (an unauthorized store tells you to runalchemy login). - The test suite passes on Windows
(#735) —
including two real product fixes:
Drizzle.Schemapassed OS-native paths to drizzle-kit (backslashes are glob escapes), andBundle.PurePlugincould be hijacked by a straypackage.jsonabovenode_modules. - Drizzle query chains are real Effects
(#750) —
db.select()...chains now expose the full Effect protocol, so they compose withEffect.alland friends instead of spinning the run loop. - PlanetScale inherited roles compare by membership
(#761) —
API-returned ordering no longer forces a
PostgresRolereplacement. Thanks Gerben Mulder! globalOutbound: nullis preserved inWorkerLoader(#746) — the documented “block all outbound network access” signal no longer silently coerces to default access. Thanks Alex!- Binding-hosted DO classes join the precreate stub
(#764) —
fixes
Worker did not expose Durable Object namespaceon fresh deploys of worker↔container cycles. Thanks Daniel Gangl! alchemy devno longer prints bun’s benign tsconfig fd warning (#768).