Worker
Source:
src/Cloudflare/Workers/Worker.ts
A Cloudflare Worker host with deploy-time binding support and runtime export collection.
A Worker follows a two-phase pattern. The outer Effect.gen runs at
deploy time to bind resources (KV, R2, Durable Objects, etc.). It returns
an object whose properties are the Worker’s runtime handlers — fetch for
HTTP requests and any additional RPC methods.
Effect.gen(function* () { // Phase 1: bind resources (runs at deploy time) const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { // Phase 2: runtime handlers (runs on each request) fetch: Effect.gen(function* () { const value = yield* kv.get("key"); return HttpServerResponse.text(value ?? "not found"); }), };})There are three ways to define a Worker, from simplest to most flexible. See the Functions & Servers page for the full explanation.
- Async — plain
async fetchhandler, no Effect runtime in the bundle. - Effect — Effect implementation passed directly, single file.
- Layer — class and
.make()in a single file; Rolldown tree-shakes.make()from consumers.
Async Workers
Section titled “Async Workers”You don’t have to use Effect for your runtime code. If you create
a Worker resource with main pointing at a file but provide no
Effect.gen implementation, Alchemy bundles and deploys that file
as-is. Your handler is a plain async fetch — no Effect runtime
is included in the bundle.
Use the env prop to declare which resources, Config values,
and literal env vars are available at runtime, and
Cloudflare.InferEnv to extract a fully typed env object from
them.
See the Workers guide for a comprehensive walkthrough of all binding types (R2, D1, Durable Objects, Assets, and more).
Defining an async Worker in your stack
const db = yield* Cloudflare.D1.Database("DB");const bucket = yield* Cloudflare.R2.Bucket("Bucket");
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { db, bucket },});Writing the async handler
import type { WorkerEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WorkerEnv) { if (request.method === "GET") { const object = await env.bucket.get("key"); return new Response(object?.body ?? null); } return new Response("Not Found", { status: 404 }); },};Effect Workers
Section titled “Effect Workers”Pass the Effect implementation as the third argument. This is the simplest Effect-based approach — everything lives in one file. Convenient for standalone Workers that don’t need to be referenced by other Workers.
export default class MyWorker extends Cloudflare.Worker<MyWorker>()( "MyWorker", { main: import.meta.url }, Effect.gen(function* () { // init: bind resources const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { // runtime: use them fetch: Effect.gen(function* () { const value = yield* kv.get("key"); return HttpServerResponse.text(value ?? "not found"); }), }; }),) {}Worker Layer
Section titled “Worker Layer”When two Workers need to reference each other (e.g. WorkerA calls
WorkerB and vice versa), or you simply want optimal tree-shaking,
define the Worker class separately from its .make() call. The
class is a lightweight identifier; .make() provides the runtime
implementation as an export default. Rolldown treats .make()
as pure, so any Worker that imports the class to bind it will not
pull in the .make() dependencies — the bundler tree-shakes
them away entirely.
The class and .make() can live in the same file. This is the
same pattern used by Container and DurableObject,
and is recommended for any cross-Worker or cross-DO bindings.
Worker Layer (class + .make() in one file)
// src/WorkerB.ts — the tag carries the name + RPC shape; props live// on `.make()`.export class WorkerB extends Cloudflare.Worker< WorkerB, { greet: (name: string) => Effect.Effect<string> }>()("WorkerB") {}
export default WorkerB.make( { main: import.meta.url }, Effect.gen(function* () { // init: bind resources const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { // runtime: use them greet: (name: string) => Effect.gen(function* () { yield* kv.put("last-greeted", name); return `Hello ${name}`; }), }; }),);Binding a Worker Layer from another Worker
// src/WorkerA.ts — imports WorkerB; bundler tree-shakes .make()import WorkerB from "./WorkerB.ts";
export default class WorkerA extends Cloudflare.Worker<WorkerA>()( "WorkerA", { main: import.meta.url }, Effect.gen(function* () { const b = yield* Cloudflare.Workers.bindWorker(WorkerB); return { fetch: Effect.gen(function* () { return yield* b.greet("world"); }), }; }),) {}Configuration
Section titled “Configuration”The props object controls compatibility flags, static assets, and build options. These are evaluated at deploy time.
Enabling Node.js compatibility
{ main: import.meta.url, compatibility: { flags: ["nodejs_compat"], date: "2026-03-17", },}Serving static assets
{ main: import.meta.url, assets: "./public",}Zone routes
{ main: import.meta.filename, routes: [ { pattern: "api.example.com/*", zoneName: "example.com" }, { pattern: "example.com/api/*", zoneId: "<YOUR_ZONE_ID>" }, ],}Deploying a prebuilt Worker without bundling
When main already points at a complete, runtime-ready ESM bundle
produced by an external tool (e.g. OpenNext), set bundle: false to
upload it byte-for-byte. The entry’s directory is walked recursively
and every file matching the module rules (by default .js, .mjs,
.wasm, .txt, .html, .sql, and .bin) is uploaded as an
additional module named by its path relative to that directory.
{ main: "./.open-next/worker.js", bundle: false, assets: "./.open-next/assets",}Observability
Section titled “Observability”Cloudflare Workers Observability is on by default — logs.enabled and
logs.invocationLogs are turned on if you don’t pass an observability
prop. Pass the prop yourself to tune sampling, enable persistence, or
turn on the new traces channel (the same toggle the dashboard’s
Observability tab writes).
Field names match the Cloudflare API (camelCased): headSamplingRate,
invocationLogs, etc.
{ main: import.meta.url, observability: { enabled: true, headSamplingRate: 1, logs: { enabled: true, invocationLogs: true, headSamplingRate: 1, persist: true, }, traces: { enabled: true, headSamplingRate: 1, persist: true, }, },}Workers Cache
Section titled “Workers Cache”Workers Cache puts a regionally tiered cache in front of the Worker —
cache hits are served from the edge without invoking the Worker (and
without billing CPU time). In an Effect-native Worker, enable it by
yielding Cloudflare.cache() in the init phase, which also returns the
runtime purge client; async Workers use the cache prop instead. Control
what gets cached from your handlers via standard response headers:
Cache-Control (including stale-while-revalidate), Cache-Tag for
tag-based purging, and Vary for content negotiation.
The cache is scoped to a single Worker version by default, so every
deploy starts cold. Set crossVersionCache: true to share cached
responses across versions.
Enabling and purging the cache in an Effect Worker
Effect.gen(function* () { // init: enable Workers Cache on this Worker 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"] }); return HttpServerResponse.text("purged"); } return HttpServerResponse.text("hello", { headers: { "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", "Cache-Tag": "products,product:123", }, }); }), };})Enabling Workers Cache on an async Worker
{ main: "./src/worker.ts", cache: { enabled: true, crossVersionCache: true, },}Background Work & Scopes
Section titled “Background Work & Scopes”Each incoming event (fetch, RPC call, scheduled run) gets its own Effect
Scope. When the handler finishes, the bridge closes that scope and
registers the close promise with workerd’s ctx.waitUntil — so
finalizers added with Effect.addFinalizer inside a handler run after
the response is sent, without blocking it, and the Worker stays alive
until they settle. Streaming responses transfer the scope to the stream,
so those finalizers run when the stream completes instead.
For ad-hoc background work, WorkerExecutionContext.waitUntil(effect)
forks an Effect with the caller’s full context and keeps the invocation
alive until it settles. The context can be yielded once in the init
closure and used from any handler; its methods are RuntimeContext-
colored, so they can only run inside a handler.
The init closure is evaluated once per isolate: the bridge builds the
Worker’s layer stack on the first event and every later event reuses the
built services. Resolve services, bind resources, build handlers there —
one-shot I/O that caches a plain value (e.g. fetching a secret for a
client) is fine, but nothing disposable: the build scope is never closed
(workerd has no isolate-teardown hook), so a finalizer added in the init
closure never runs, and I/O-backed objects (sockets, response bodies) are
pinned to the request that created them. Anything that needs cleanup
belongs in a handler, where Effect.addFinalizer attaches to the
per-event scope.
Post-response cleanup with a scope finalizer
return { fetch: Effect.gen(function* () { // runs after this response is sent, kept alive by waitUntil yield* Effect.addFinalizer(() => flushMetrics().pipe(Effect.ignore)); return HttpServerResponse.text("ok"); }),};Background work with waitUntil
// initconst exec = yield* Cloudflare.WorkerExecutionContext;
return { fetch: Effect.gen(function* () { // respond now; the audit write completes in the background yield* exec.waitUntil(writeAuditLog(event)); return HttpServerResponse.text("accepted", { status: 202 }); }),};R2 Bucket
Section titled “R2 Bucket”Bind an R2 bucket in the init phase with Cloudflare.R2.ReadWriteBucket.
The returned handle exposes get, put, delete, and list
methods you can call in your runtime handlers.
// initconst bucket = yield* Cloudflare.R2.ReadWriteBucket(MyBucket);
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const key = request.url.split("/").pop()!;
if (request.method === "GET") { const object = yield* bucket.get(key); return object ? HttpServerResponse.text(yield* object.text()) : HttpServerResponse.empty({ status: 404 }); }
yield* bucket.put(key, request.stream); return HttpServerResponse.empty({ status: 201 }); }),};KV Namespace
Section titled “KV Namespace”Bind a KV namespace with Cloudflare.KV.ReadWriteNamespace. KV provides
eventually-consistent, low-latency key-value reads replicated
globally across Cloudflare’s edge.
// initconst kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { fetch: Effect.gen(function* () { const value = yield* kv.get("my-key"); return HttpServerResponse.text(value ?? "not found"); }),};D1 Database
Section titled “D1 Database”Bind a D1 database with Cloudflare.D1.QueryDatabase. D1 is a
serverless SQLite database — use prepare to build parameterized
queries and all, first, or run to execute them.
// initconst db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
return { fetch: Effect.gen(function* () { const results = yield* db .prepare("SELECT * FROM users WHERE id = ?") .bind(userId) .all(); return yield* HttpServerResponse.json(results); }),};Durable Objects
Section titled “Durable Objects”Yield a DurableObject class in the init phase to get a
namespace handle. Call getByName or getById to get a typed RPC
stub, then call its methods from your runtime handlers.
// initconst counters = yield* Counter;
return { fetch: Effect.gen(function* () { const counter = counters.getByName("user-123"); const value = yield* counter.increment(); return HttpServerResponse.text(String(value)); }),};Containers
Section titled “Containers”Containers run long-lived processes alongside Durable Objects.
Provide Cloudflare.Containers.layer(Sandbox, …) on a DO’s init to
bind, start, and monitor the container; then yield* Sandbox
resolves the running instance. Call its typed methods or use
getTcpPort to make HTTP requests to its exposed ports.
export default class Agent extends Cloudflare.DurableObject<Agent>()( "Agents", Effect.gen(function* () { const sandbox = yield* Sandbox;
return Effect.gen(function* () { return { exec: (cmd: string) => sandbox.exec(cmd), health: () => Effect.gen(function* () { const { fetch } = yield* sandbox.getTcpPort(3000); const res = yield* fetch( HttpClientRequest.get("http://container/health"), ); return yield* res.text; }), }; }); }).pipe( Effect.provide( Cloudflare.Containers.layer(Sandbox, { enableInternet: true }), ), ),) {}Dynamic Workers
Section titled “Dynamic Workers”WorkerLoader lets you spin up ephemeral Workers at runtime
from inline JavaScript modules. This is useful for sandboxing
user-provided code or running untrusted scripts in isolation.
// initconst loader = yield* Cloudflare.WorkerLoader("Loader");
return { fetch: Effect.gen(function* () { const worker = yield* loader.load({ compatibilityDate: "2026-01-28", mainModule: "worker.js", modules: { "worker.js": `export default { async fetch(req) { return new Response("sandboxed"); } }`, }, });
const res = yield* worker.fetch( HttpClientRequest.get("https://worker/"), ); return HttpServerResponse.fromClientResponse(res); }),};