Skip to content

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 — 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 },
});

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).

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.

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):

Terminal window
alchemy sync ./alchemy.run.ts --stage prod # detect + repair
alchemy sync ./alchemy.run.ts --stage prod --dry-run # detect only

Per 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.

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 workflow
const approval = yield* Cloudflare.Workflows.waitForEvent<{ approved: boolean }>(
"approval",
{ type: "approval", timeout: "1 day" },
);
// from outside
yield* instance.sendEvent({ type: "approval", payload: { approved: true } });

createBatch, restart, rollback status, and extended event metadata round out the surface. Docs: Workflows.

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.

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’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 },
},
},
});

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.

  • 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’s read/diff is 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.ref values bind natively in Worker env (#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 run alchemy login).
  • The test suite passes on Windows (#735) — including two real product fixes: Drizzle.Schema passed OS-native paths to drizzle-kit (backslashes are glob escapes), and Bundle.PurePlugin could be hijacked by a stray package.json above node_modules.
  • Drizzle query chains are real Effects (#750) — db.select()... chains now expose the full Effect protocol, so they compose with Effect.all and friends instead of spinning the run loop.
  • PlanetScale inherited roles compare by membership (#761) — API-returned ordering no longer forces a PostgresRole replacement. Thanks Gerben Mulder!
  • globalOutbound: null is preserved in WorkerLoader (#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 namespace on fresh deploys of worker↔container cycles. Thanks Daniel Gangl!
  • alchemy dev no longer prints bun’s benign tsconfig fd warning (#768).