Skip to content

Cloudflare.Workers reference

Source: src/Cloudflare/Workers/AccountSetting.ts

The account-wide Workers settings singleton (/accounts/{account_id}/workers/account-settings): the default usage model for new Workers and the Green Compute flag for scheduled Workers.

This is a singleton — it always exists on every account with Cloudflare defaults, so this resource never creates or deletes anything physical. Reconcile PUTs the settings when the observed values differ from the desired ones; destroy restores the values the account had before Alchemy first managed it (captured as initialDefaultUsageModel / initialGreenCompute).

Enable Green Compute for scheduled Workers

yield* Cloudflare.Workers.AccountSetting("GreenCompute", {
greenCompute: true,
});

Pin the default usage model

yield* Cloudflare.Workers.AccountSetting("UsageModel", {
defaultUsageModel: "standard",
greenCompute: false,
});

Source: src/Cloudflare/Workers/AI.ts

The native Cloudflare Workers AI binding — run inference on Workers AI models directly from a Worker, with no AI Gateway (or any other cloud resource) required. This is the plain { type: "ai" } Worker binding: the runtime value is the same env.AI handle you would declare in wrangler.json.

AI is a single value that is at once the Binding.Service tag, the callable that produces an AIBinding, and the type. Declare it on a Worker’s env (it flows through InferEnv → the runtime Ai handle) or yield* it inside an Effect-native Worker to attach the binding and obtain the AIClient.

Use Cloudflare.AI.Gateway + QueryGateway instead when you want requests routed through an AI Gateway (caching, rate limiting, logs); use AI when you just want to call Workers AI models.

Cloudflare.Worker("AiWorker", { main: import.meta.url },
Effect.gen(function* () {
const ai = yield* Cloudflare.Workers.AI();
return {
fetch: Effect.gen(function* () {
const result = yield* ai.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", {
prompt: "What is the origin of the phrase Hello, World?",
}).pipe(Effect.orDie);
return yield* HttpServerResponse.json(result);
}),
};
}).pipe(Effect.provide(Cloudflare.Workers.AIBinding)),
);

model(options) produces a Layer<LanguageModel, never, RuntimeContext> that translates LanguageModel.generateText / streamText calls (including tool calls) into ai.run(...) against the bound Workers AI model — the same adapter AI Gateway’s QueryGateway uses, minus the gateway routing.

const ai = yield* Cloudflare.Workers.AI();
const languageModel = ai.model({
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
parameters: { temperature: 0.7, maxTokens: 1024 },
});
const response = yield* LanguageModel.generateText({ prompt }).pipe(
Effect.provide(languageModel),
);
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { AI: Cloudflare.Workers.AI() },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { AI: Ai }

Source: src/Cloudflare/Workers/Browser.ts

A Cloudflare Browser Rendering binding for launching headless browser sessions from Workers — a Worker-only binding with no backing cloud resource.

Browser is a single value that is at once the Binding.Service tag, the callable that produces a BrowserBinding, and the type. Declare it on a Worker’s env (it flows through InferEnvcf.BrowserRun) or yield* it inside an Effect-native Worker to attach the binding and obtain the BrowserClient.

Section titled “Browser: Effect-style Worker (recommended)”
import * as Effect from "effect/Effect";
Cloudflare.Worker(
"BrowserWorker",
{ main: import.meta.url },
Effect.gen(function* () {
const browser = yield* Cloudflare.Browser("BROWSER");
return {
fetch: Effect.gen(function* () {
return yield* browser.markdown({ url: "https://example.com" });
}),
};
}).pipe(Effect.provide(Cloudflare.Workers.BrowserBinding)),
);
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { BROWSER: Cloudflare.Browser() },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { BROWSER: BrowserRun }
// Default: a real headless Chrome is launched locally and driven over
// CDP under `alchemy dev`. Alchemy.remote() opts the binding into the
// real Browser Rendering service instead — in an Effect-native Worker:
const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote());
// or declared on an async Worker's env:
env: { BROWSER: Cloudflare.Browser("BROWSER").pipe(Alchemy.remote()) }

Source: src/Cloudflare/Workers/Cache.ts

Enable Workers Cache on the surrounding Worker and get the runtime cache client.

Yielding cache() in an Effect-native Worker’s init phase turns the cache on at deploy time (the equivalent of setting cache: { enabled: true } on the Worker’s props) and returns a client whose purge drives the runtime ctx.cache API from your handlers.

What gets cached is controlled by standard response headers — Cache-Control (including stale-while-revalidate), Cache-Tag for tag-based purging, and Vary for content negotiation.

For async (non-Effect) Workers, set the cache prop on the Worker instead.

Effect.gen(function* () {
// init: enable Workers Cache on this Worker
const { purge } = yield* Cloudflare.cache();
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",
"Cache-Tag": "products",
},
});
}),
};
})

Source: src/Cloudflare/Workers/CronEventSource.ts

Subscribe to Cloudflare Cron Triggers with an Effect handler.

A single call wires both pieces of a scheduled Worker:

  • Deploy-time: attaches the cron expression to the host Worker’s Cron Triggers.

  • Runtime: registers a scheduled listener that runs your Effect on each fire. The handler receives Cloudflare’s ScheduledController, which has three members:

    • controller.scheduledTime — the fire time in milliseconds since the Unix epoch.
    • controller.cron — the cron expression that fired.
    • controller.noRetry() — opts the invocation out of Cloudflare’s retry-on-failure. Only meaningful when the scheduled invocation can actually fail — see the failure & retry section below.

    Each member has its own section below with an Effect example first and an async example second.

Requires CronEventSourceLive provided on the Worker’s Effect.

Failure & retry semantics: a failing handler won’t crash the Worker — the event source catches the failure and moves on. That also means Cloudflare never observes a failed invocation, so its platform-level retry (and controller.noRetry()) never comes into play here. Express retry declaratively with Effect.retry inside the handler, and log or report errors if you need visibility into failed runs. In async Workers the opposite holds: a scheduled handler that throws (or rejects) marks the invocation failed and Cloudflare may retry it — call controller.noRetry() before rethrowing to suppress that.

Async (non-Effect) Workers don’t use cron — they attach schedules with the Worker’s crons prop and export their own scheduled handler from the entry module (each section below includes the async variant). Pass crons: [] to remove all Cron Triggers from a Worker.

Effect-native Worker (recommended)

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default Cloudflare.Worker(
"Worker",
{ main: import.meta.url },
Effect.gen(function* () {
yield* Cloudflare.Workers.cron("0 12 * * *", () =>
Effect.log("cron fired"),
);
return {
fetch: Effect.succeed(HttpServerResponse.text("ok")),
};
}).pipe(Effect.provide(Cloudflare.Workers.CronEventSourceLive)),
);

Async Worker — crons prop + exported scheduled handler

// alchemy.run.ts — attach the cron expressions at deploy time
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
crons: ["0 12 * * *"],
});
// src/worker.ts — the entry module handles the fires itself
export default {
async scheduled(controller: ScheduledController) {
console.log("cron fired");
},
};

cron: controller.scheduledTime — the fire time

Section titled “cron: controller.scheduledTime — the fire time”

Effect: record each fire on a Durable Object

export default class Worker extends Cloudflare.Worker<Worker>()(
"Worker",
{ main: import.meta.url },
Effect.gen(function* () {
const counters = yield* CronCounter;
yield* Cloudflare.Workers.cron("0 * * * *", (controller) =>
counters.getByName("default").record(controller.scheduledTime),
);
return {
fetch: Effect.gen(function* () {
const { times } = yield* counters.getByName("default").snapshot();
return yield* HttpServerResponse.json({ times });
}),
};
}).pipe(Effect.provide(Cloudflare.Workers.CronEventSourceLive)),
) {}

Async: use the fire time as an idempotency key

// scheduledTime is the time the fire was *scheduled* for (not when it
// ran), so it is stable across retries of the same fire — a natural
// idempotency key for at-most-once side effects.
export default {
async scheduled(controller: ScheduledController, env: WorkerEnv) {
const key = `run:${controller.scheduledTime}`;
if (await env.RUNS.get(key)) return;
await syncFeeds();
await env.RUNS.put(key, "done");
},
};

cron: controller.cron — dispatch multiple schedules

Section titled “cron: controller.cron — dispatch multiple schedules”

Effect: one handler per expression

// Each handler only runs for fires of its own expression — the listener
// checks controller.cron, so a midnight fire never runs the hourly handler.
yield* Cloudflare.Workers.cron("0 * * * *", () => syncFeeds);
yield* Cloudflare.Workers.cron("0 0 * * *", () => purgeExpired);

Async: switch on controller.cron

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
crons: ["0 * * * *", "0 0 * * *"],
});
// src/worker.ts — one scheduled handler receives every fire
export default {
async scheduled(controller: ScheduledController) {
switch (controller.cron) {
case "0 * * * *":
await syncFeeds();
break;
case "0 0 * * *":
await purgeExpired();
break;
}
},
};

cron: controller.noRetry() — failure & retry control

Section titled “cron: controller.noRetry() — failure & retry control”

Effect: bound retries with Effect.retry

import * as Schedule from "effect/Schedule";
// The event source reports the invocation as successful even when the
// handler fails, so Cloudflare's platform retry rarely engages for Effect
// handlers — Effect.retry is the primary retry control. Calling
// controller.noRetry() once retries are exhausted defensively covers
// anything that can still mark the invocation failed (e.g. a failing
// waitUntil task).
yield* Cloudflare.Workers.cron("0 * * * *", (controller) =>
syncFeeds.pipe(
Effect.retry({ schedule: Schedule.exponential("1 second"), times: 3 }),
Effect.tapError((error) =>
Effect.logError("syncFeeds failed permanently", error).pipe(
Effect.andThen(Effect.sync(() => controller.noRetry())),
),
),
),
);

Async: suppress retry for permanent failures

// src/worker.ts — a thrown error marks the invocation failed and
// Cloudflare may retry it; noRetry() opts this fire out of that.
export default {
async scheduled(controller: ScheduledController) {
try {
await syncFeeds();
} catch (error) {
if (isPermanentFailure(error)) {
controller.noRetry();
}
throw error; // still recorded as a failed invocation
}
},
};

Source: src/Cloudflare/Workers/DurableObject.ts

A Cloudflare Durable Object namespace that manages globally unique, stateful instances with WebSocket hibernation support.

A Durable Object uses a two-phase pattern with two nested Effect.gen blocks. The outer Effect resolves shared dependencies (other DOs, containers, etc.) and the instance state reference (Cloudflare.DurableObjectState). The inner Effect returns the object’s public methods and WebSocket handlers — and is the only place the state’s RuntimeContext-colored methods (storage.get, storage.put, …) can actually run.

Effect.gen(function* () {
// Phase 1: resolve shared dependencies + the instance state ref
const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
// Phase 2: per-instance setup and public API. `state`'s methods
// (storage.get/put, …) are RuntimeContext-colored, so the actual
// I/O happens here, in the runtime closure.
return {
save: (data: string) => db.exec("INSERT ..."),
fetch: Effect.gen(function* () { ... }),
webSocketMessage: Effect.fn(function* (ws, msg) { ... }),
};
});
})

There are two ways to define a Durable Object. See the Runtime page for the full explanation.

  • Inline — Effect implementation passed directly, single file.
  • Modular — class and implementation in separate files for tree-shaking.

Pass the Effect implementation as the second argument. This is the simplest approach — everything lives in one file. Convenient when the DO doesn’t need to be referenced by other Workers or DOs that would pull in its runtime dependencies.

export default class Counter extends Cloudflare.DurableObject<Counter>()(
"Counter",
Effect.gen(function* () {
// init: bind resources + resolve the instance state ref
const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
// runtime: state's storage methods are RuntimeContext-colored,
// so the reads/writes live here
const count = (yield* state.storage.get<number>("count")) ?? 0;
return {
increment: () =>
Effect.gen(function* () {
const next = count + 1;
yield* state.storage.put("count", next);
return next;
}),
get: () => Effect.succeed(count),
};
});
}),
) {}

When a Worker and a DO reference each other, or multiple Workers bind the same DO, define the 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 the bundler tree-shakes it and all its runtime dependencies out of any consumer’s bundle.

The class and .make() can live in the same file. This is the same pattern used by Worker and Container.

Modular Durable Object (class + .make() in one file)

src/Counter.ts
export class Counter extends Cloudflare.DurableObject<Counter>()(
"Counter",
) {}
export default Counter.make(
Effect.gen(function* () {
// init: bind resources + resolve the instance state ref
const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
// runtime: state's storage methods are RuntimeContext-colored,
// so the reads/writes live here
const count = (yield* state.storage.get<number>("count")) ?? 0;
return {
increment: () =>
Effect.gen(function* () {
const next = count + 1;
yield* state.storage.put("count", next);
yield* db.prepare("INSERT INTO logs (count) VALUES (?)").bind(next).run();
return next;
}),
get: () => Effect.succeed(count),
};
});
}),
);

Binding a modular DO from a Worker

// imports Counter; bundler tree-shakes .make()
import Counter from "./Counter.ts";
// init
const counters = yield* Counter;
return {
fetch: Effect.gen(function* () {
const counter = counters.getByName("user-123");
return HttpServerResponse.text(String(yield* counter.get()));
}),
};

A Durable Object is hosted by exactly one Worker, but any number of other Workers can bind to the same DO. This is how you share state across Workers: one Worker hosts the DO, every other Worker addresses it by scriptName and gets a typed stub.

To make this type-safe, the host Worker must declare the DO as part of its public contract via the third type argument to Cloudflare.Worker<Self, Bindings, Deps>(). Deps is the set of DO classes (or other Workers) the script exposes for other scripts to bind to.

Host Worker declares the DO in its contract

// workerA.ts — hosts Counter
import { Counter, CounterLive } from "./object.ts";
// ^^^^^^^ declared as part of WorkerA's public contract
export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()(
"WorkerA",
{ main: import.meta.url },
) {}
// WorkerA's Layer also provides the DO's Live implementation.
export default WorkerA.make(
Effect.gen(function* () {
const counter = yield* Counter;
return { fetch: Effect.gen(function* () { ... }) };
}).pipe(Effect.provide(CounterLive)),
);

Consumer Worker binds the DO via Counter.from(WorkerA)

// workerB.ts — binds to the same Counter, hosted by WorkerA
import { Counter } from "./object.ts";
import { WorkerA } from "./workerA.ts";
export default class WorkerB extends Cloudflare.Worker<WorkerB>()(
"WorkerB",
{ main: import.meta.url },
Effect.gen(function* () {
// ^^^^^^^^^^^^ scriptName-bound stub of WorkerA's Counter
const counter = yield* Counter.from(WorkerA);
return {
fetch: Effect.gen(function* () {
const value = yield* counter.getByName("shared").get();
return HttpServerResponse.text(String(value));
}),
};
}),
) {}

Only the host Worker’s Stack provides CounterLive — the consumer Worker just imports the Counter class as a typed identifier. Rolldown tree-shakes CounterLive (and its dependencies) out of WorkerB’s bundle.

DurableObject: Using .from(Self) Inside the Host

Section titled “DurableObject: Using .from(Self) Inside the Host”

Inside the host Worker, yield* Counter and yield* Counter.from(Self) resolve to the same local namespace. The .from(Self) form is preferred — especially in code that may be extracted into a reusable Layer — because it makes the scriptName explicit and lets the same Layer shape work whether the consumer is the host or another script.

Counter.from(WorkerA) inside WorkerA itself

// workerA.ts — host uses `.from(Self)` instead of bare `yield* Counter`
export default WorkerA.make(
Effect.gen(function* () {
const counter = yield* Counter.from(WorkerA); // same as `yield* Counter`
return { fetch: Effect.gen(function* () { ... }) };
}).pipe(Effect.provide(CounterLive)),
);

A Worker can also host its own isolated namespace this way. If a second host Worker declares Counter in its contract and provides CounterLive, the DO instances under that script are separate from the original host’s — same class, two namespaces.

Two hosts, two isolated namespaces

// workerC.ts — another host of Counter, isolated from WorkerA
export class WorkerC extends Cloudflare.Worker<WorkerC, {}, Counter>()(
"WorkerC",
{ main: import.meta.url },
) {}
export default WorkerC.make(
Effect.gen(function* () {
// .from(WorkerC) binds to WorkerC's own Counter namespace —
// writes here are NOT visible from WorkerA's Counter.
const counter = yield* Counter.from(WorkerC);
return { fetch: Effect.gen(function* () { ... }) };
}).pipe(Effect.provide(CounterLive)),
);

Any function you return from the inner Effect becomes an RPC method that Workers can call through a stub. Methods must return an Effect. The caller gets a fully typed stub — if your DO returns increment and get, the stub exposes counter.increment() and counter.get().

return {
increment: () => Effect.succeed(++count),
get: () => Effect.succeed(count),
reset: () => Effect.sync(() => { count = 0; }),
};

RPC methods can return an Effect Stream and the caller will see the chunks as they’re produced. Combine with Stream.schedule to pace emission, or with Stream.fromQueue to bridge an inbound subscription.

Streaming sequential numbers

import * as Schedule from "effect/Schedule";
import * as Stream from "effect/Stream";
return {
tick: (n: number) =>
Stream.iterate(0, (i) => i + 1).pipe(
Stream.take(n),
Stream.schedule(Schedule.spaced("100 millis")),
),
};

Forwarding the stream as a chunked HTTP response

// in a Worker fetch handler
const counter = counters.getByName("tick");
const stream = counter.tick(5).pipe(
Stream.map((i) => `${i}\n`),
Stream.encodeText,
);
return HttpServerResponse.stream(stream, {
headers: { "content-type": "text/plain" },
});

DurableObject: Worker → DO HTTP forwarding

Section titled “DurableObject: Worker → DO HTTP forwarding”

In addition to RPC methods, the typed stub exposes a fetch method that forwards an HttpServerRequest straight to the DO. The DO’s own fetch Effect produces the response — useful for WebSocket upgrades and other request-shaped interactions.

const room = rooms.getByName(roomId);
return yield* room.fetch(request);

DurableObject: Placing a Durable Object in a Region

Section titled “DurableObject: Placing a Durable Object in a Region”

A Durable Object is created wherever its first-ever request came from, and it stays there for life. That default is right for an instance whose traffic all comes from the user who created it, and wrong for one whose name is derived from something other than geography (a shard index, a hash, a fixed roster) — a Sydney user routed onto an instance that some Frankfurt request happened to create first pays a cross-planet round trip on every call.

Pass a locationHint to place the instance near the traffic you expect instead. It only applies to creation: an instance that already exists is unaffected, so a hint can’t move a live DO.

// Both the name and the hint derive from the caller's region, so
// each shard is created in the region whose users address it.
const region = hintFor(request.cf?.continent); // "apac", "weur", …
const shard = shards.getByName(`pool:${region}:${index}`, {
locationHint: region,
});

Each Durable Object instance has its own transactional key-value storage via Cloudflare.DurableObjectState. Resolve the state reference in the outer (init) Effect, but call its methods — storage.get, storage.put, … — only from the inner (runtime) Effect: those methods are RuntimeContext-colored, so the type system only allows them inside the runtime closure.

// inner (runtime) Effect — `state` was resolved in the outer Effect
yield* state.storage.put("counter", 42);
const value = yield* state.storage.get("counter");

Every RPC call and fetch into a Durable Object gets its own Effect Scope. When the method finishes, the bridge closes that scope and registers the close promise with workerd’s state.waitUntil — so finalizers added with Effect.addFinalizer inside a method run after the result is returned to the caller, without blocking it, and the object stays alive until they settle.

For ad-hoc background work, state.waitUntil(effect) forks an Effect with the caller’s full context and keeps the object alive until it settles.

Attach cleanup to method scopes, not the constructor (init) closure — the constructor runs once per in-memory instance under blockConcurrencyWhile, and its scope is not tied to any call.

return {
record: Effect.fn(function* (entry: string) {
// runs after `record` returns; the DO stays alive until it settles
yield* Effect.addFinalizer(() =>
state.storage.put(`audit:${entry}`, Date.now()).pipe(Effect.ignore),
);
return "accepted" as const;
}),
refresh: Effect.fn(function* () {
// same idea, explicit form
yield* state.waitUntil(recomputeExpensiveView());
return "scheduled" as const;
}),
};

Durable Objects support WebSocket hibernation — the runtime can evict the object from memory while keeping connections open. Use Cloudflare.upgrade() to accept a connection, and return webSocketMessage / webSocketClose / webSocketError handlers to process events when the object wakes back up.

Accepting a WebSocket connection

return {
fetch: Effect.gen(function* () {
const [response, socket] = yield* Cloudflare.upgrade();
socket.serializeAttachment({ id: crypto.randomUUID() });
return response;
}),
};

Handling messages and close events

return {
webSocketMessage: Effect.fn(function* (
socket: Cloudflare.WebSocket,
message: string | Uint8Array,
) {
const text = typeof message === "string"
? message
: new TextDecoder().decode(message);
// process the message
}),
webSocketClose: Effect.fn(function* (
ws: Cloudflare.WebSocket,
code: number,
reason: string,
) {
yield* ws.close(code, reason);
}),
webSocketError: Effect.fn(function* (
ws: Cloudflare.WebSocket,
error: unknown,
) {
// the runtime closes the socket afterwards; clear its session here
ws.serializeAttachment(null);
}),
};

Recovering sessions after hibernation

Resolve the state reference in the outer Effect, but place the rehydration loop (state.getWebSockets() is RuntimeContext-colored) inside the inner Effect.gen so it runs every time the DO instance is reconstructed (including after Cloudflare wakes the DO from hibernation).

Effect.gen(function* () {
const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
const sessions = new Map<string, Cloudflare.WebSocket>();
// Rehydrate the in-memory session map after hibernation.
for (const socket of yield* state.getWebSockets()) {
const data = socket.deserializeAttachment<{ id: string }>();
if (data) sessions.set(data.id, socket);
}
return {
fetch: Effect.gen(function* () {
const [response, socket] = yield* Cloudflare.upgrade();
const id = crypto.randomUUID();
socket.serializeAttachment({ id });
sessions.set(id, socket);
return response;
}),
webSocketMessage: Effect.fn(function* (socket, message) {
const text =
typeof message === "string" ? message : new TextDecoder().decode(message);
for (const peer of sessions.values()) {
yield* peer.send(text);
}
}),
};
});
});

Register Alchemy.makeCallback handlers in the inner, per-instance Effect. Durable Objects supply callback registration on their instance RuntimeContext using SQLite and native alarms. Scheduling participates in the current storage transaction, and each job is acknowledged only after its handler succeeds. No explicit alarm handler is needed.

const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
const onArchive = yield* Alchemy.makeCallback(
"archive",
Effect.fn(function* (payload: { key: string; body: string }) {
yield* archive.put(payload.key, payload.body);
}),
);
return {
save: Effect.fn(function* (id: string, body: string) {
yield* state.storage.transaction(
Effect.gen(function* () {
yield* state.storage.put(id, body);
yield* onArchive.schedule(id, {
after: "30 seconds",
payload: { key: id, body },
});
}),
);
}),
};
});

Callbacks receive JSON-serializable payloads and deliver at least once, so external writes must be idempotent. A recovery wake is persisted before each attempt; configure its delay with the third argument, { retry: { delay: "1 minute" } }. Scheduling the same callback name and ID replaces the pending job; onArchive.cancel(id) cancels it. Retain handlers for old callback names while their jobs are pending. Each native alarm processes up to 100 due jobs; direct setAlarm/deleteAlarm calls bypass the scheduler’s coordination. Leave native alarm retries enabled when aborting an instance. Passing { retryAlarm: false } removes the automatic-recovery guarantee: Cloudflare can suppress a replacement wake even after its timestamp is persisted. Jobs remain stored, but may need an explicitly rearmed native alarm.

The scheduler migrates its original unversioned SQLite schema to version 1 atomically, preserving existing events. Old events still use the explicit alarm handler below; their rows have no callback name to infer. Both APIs coordinate the same native alarm. Unknown newer schema versions fail closed.

Each Durable Object can have a single alarm timestamp. Alchemy layers a small SQLite-backed scheduler on top via Cloudflare.Workers.scheduleEvent and Cloudflare.Workers.processScheduledEvents, so you can register many named events with arbitrary payloads and fire them from a single alarm handler.

// schedule from a request or message handler
yield* Cloudflare.Workers.scheduleEvent(
"reminder-1",
new Date(Date.now() + 60_000),
{ message: "your meeting starts in a minute" },
);
return {
alarm: () =>
Effect.gen(function* () {
const fired = yield* Cloudflare.Workers.processScheduledEvents;
for (const event of fired) {
const payload = event.payload as { message: string };
// dispatch / broadcast / persist...
}
}),
};

state.abort(reason?, options?) forcibly resets the isolate. By default an in-progress alarm retries after the reset. Pass { retryAlarm: false } when the alarm should stop instead — for example an alarm that deletes storage so the constructor does not recreate it.

export class CleanupTask extends Cloudflare.DurableObject<CleanupTask>()(
"CleanupTask",
Effect.gen(function* () {
const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
// This won't be re-run after the alarm is aborted
yield* state.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS foo (
id INTEGER PRIMARY KEY
)
`);
return {
alarm: () =>
Effect.gen(function* () {
yield* state.storage.sql.exec("DROP TABLE foo");
yield* state.abort("Cleanup complete", { retryAlarm: false });
}),
};
});
}),
) {}

Yield the DO class in your Worker’s init phase to get a namespace handle. Call getByName or getById to get a typed stub, then call any RPC method or forward an HTTP request with fetch.

Calling RPC methods

// init
const counters = yield* Counter;
return {
fetch: Effect.gen(function* () {
const counter = counters.getByName("user-123");
yield* counter.increment();
const value = yield* counter.get();
return HttpServerResponse.text(String(value));
}),
};

Forwarding an HTTP request

// init
const rooms = yield* Room;
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const room = rooms.getByName(roomId);
return yield* room.fetch(request);
}),
};

When using an Async Worker (plain async fetch handler, no Effect runtime), declare Durable Objects in the bindings prop of the Worker resource. Pass a DurableObject reference with a className matching the exported DurableObject subclass in your worker source file. If className is omitted, it defaults to the namespace name. Use Cloudflare.InferEnv to get a fully typed env object that includes the namespace.

Declaring a DO binding in the stack

alchemy.run.ts
import type { Counter } from "./src/worker.ts";
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: {
Counter: Cloudflare.DurableObject<Counter>("Counter"),
},
});

Using the DO from a plain async handler

src/worker.ts
import { DurableObject } from "cloudflare:workers";
import type { WorkerEnv } from "../alchemy.run.ts";
export default {
async fetch(request: Request, env: WorkerEnv) {
const counter = env.Counter.getByName("my-counter");
const count = await counter.increment();
return new Response(JSON.stringify({ count }));
},
};
export class Counter extends DurableObject {
private counter = 0;
async increment() {
return ++this.counter;
}
}

DurableObject: Cross-Script Binding in an Async Worker

Section titled “DurableObject: Cross-Script Binding in an Async Worker”

Async Workers can also bind to a Durable Object hosted by another Worker script. The host Worker declares and exports the DO class. The consumer Worker declares a DurableObject with scriptName set to the host Worker’s script name.

Cross-script async bindings are references only: the consumer uploads the binding metadata, but Alchemy does not drive class migrations for the foreign class. Deploy the host first so Cloudflare can verify that the target script exports the requested class.

Host Worker owns the Durable Object class

const host = yield* Cloudflare.Worker("Host", {
main: "./src/host.ts",
bindings: {
Counter: Cloudflare.DurableObject<Counter>("Counter"),
},
});

Consumer Worker binds to the host script

const consumer = yield* Cloudflare.Worker("Consumer", {
main: "./src/consumer.ts",
bindings: {
Counter: Cloudflare.DurableObject<Counter>("Counter", {
scriptName: host.workerName,
}),
},
});

Binding to a different exported class name

const consumer = yield* Cloudflare.Worker("Consumer", {
main: "./src/consumer.ts",
bindings: {
Counter: Cloudflare.DurableObject<Counter>("Counter", {
className: "CounterV2",
scriptName: host.workerName,
}),
},
});

DurableObject: Container-Backed Durable Objects in an Async Worker

Section titled “DurableObject: Container-Backed Durable Objects in an Async Worker”

A container-backed class is declared by binding a Cloudflare.Container directly in the async Worker’s env — the Container is the Durable Object binding plus its ContainerApplication. See the Async Workers section on Container for the full walkthrough.

// `Sandbox` is the container-backed DO class exported by the worker
// script (extends `@cloudflare/containers`' `Container`).
import type { Sandbox } from "./src/worker.ts";
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: {
Sandbox: Cloudflare.Container<Sandbox>("Sandbox", {
image: "docker.io/cloudflare/sandbox:0.1.3",
}),
},
});

DurableObject: Moving a Class Between Workers

Section titled “DurableObject: Moving a Class Between Workers”

A Durable Object class can move from one Worker to another with its data intact. The move is always declared — a class that disappears from one worker and appears on another is otherwise ambiguous between “transfer the data” and “delete it, start fresh”, so Alchemy never guesses (removing a DO deletes it; that is the default). Declare transferredFrom on the Durable Object at its new host, naming the former host, and the new host’s deploy ships Cloudflare’s data-preserving transferred_classes migration. The former host’s deploy converges on its own — no delete migration is emitted for a class that moved away.

Each transferredFrom entry is either the former host’s Worker logical id (same stack + stage, resolved via alchemy’s ownership tags) or its physical script name (required for cross-stack moves). When no listed host holds the namespace — a fresh stage, or the transfer already completed — the declaration is inert, so it is safe to leave in place indefinitely.

Move a class from WorkerB to WorkerA

// BEFORE: worker-b hosts the class
const b = yield* Cloudflare.Worker("WorkerB", {
main: "./src/worker-b.ts", // exports MyDOClass
bindings: {
MyDO: Cloudflare.DurableObject("MyDO", { className: "MyDOClass" }),
},
});
// AFTER: worker-a hosts it and declares where it came from;
// worker-b keeps a cross-script reference.
const a = yield* Cloudflare.Worker("WorkerA", {
main: "./src/worker-a.ts", // now exports MyDOClass
bindings: {
MyDO: Cloudflare.DurableObject("MyDO", {
className: "MyDOClass",
transferredFrom: "WorkerB", // logical id (or its script name)
}),
},
});
const b = yield* Cloudflare.Worker("WorkerB", {
main: "./src/worker-b.ts", // no longer exports MyDOClass
bindings: {
MyDO: Cloudflare.DurableObject("MyDO", {
className: "MyDOClass",
scriptName: a.workerName,
}),
},
});

Forgetting the declaration is safe: when the former host still references the class cross-script, its deploy fails before any upload with DurableObjectTransferRequired, telling you exactly what to declare — data is never silently destroyed or forked.

Chained moves keep the host history

// The class moved WorkerB → WorkerA last release and WorkerA →
// WorkerC this release. Keep the full history so a stage that lagged
// behind (or skipped the intermediate release) still transfers from
// wherever its namespace currently lives.
Cloudflare.DurableObject("MyDO", {
className: "MyDOClass",
transferredFrom: ["WorkerB", "WorkerA"],
})

Two rules for multi-worker migrations:

  • Pure moves (the former host drops the DO entirely, keeping no cross-script reference) must be two deploys: first add the class to the new host with transferredFrom and deploy; then remove it from the former host and deploy. In a single deploy nothing orders the transfer before the former host’s delete.
  • Cross-stack moves deploy the new host’s stack first, naming the former host by physical script name; the former host’s stack deploys after and converges.

DurableObject: Adopting an Existing Durable Object

Section titled “DurableObject: Adopting an Existing Durable Object”

When you adopt a Worker that already exists on Cloudflare — created outside Alchemy via Wrangler, the dashboard, or the raw API — its Durable Object classes are adopted along with it. You opt in to the takeover the same way you adopt any foreign resource: with adopt(true) (or the --adopt CLI flag), since Worker.read reports a worker with no Alchemy ownership tags as Unowned.

Alchemy normally tracks which class backs each binding through an alchemy:dos: metadata tag it writes on the script (mapping each binding’s logical id to its class name). A foreign worker has no such tag, so on the adopting deploy Alchemy falls back to matching your binding to the live class by binding name. The class is then reused in place — not recreated — so Cloudflare’s migration engine doesn’t reject the upload for creating a class that already exists.

The consequence is a one-time constraint: on the adopting deploy the binding’s className must match the class that already exists on the worker. You cannot rename the class in the same deploy that adopts it. Once the deploy completes, Alchemy owns the worker and has written the alchemy:dos: tag, so subsequent renames are driven by logical id and work normally.

Adopting a worker whose Counter class already exists

// The worker + `Counter` class were created outside Alchemy.
// `className` must match the existing class on this first deploy.
const worker = yield* Cloudflare.Worker("Worker", {
name: "existing-worker",
main: "./src/worker.ts",
bindings: {
Counter: Cloudflare.DurableObject<Counter>("Counter"),
},
}).pipe(adopt(true));

Renaming the class — only after adoption

// A SECOND deploy, after the one above. Alchemy now owns the worker
// and maps the binding by logical id, so the class can be renamed.
const worker = yield* Cloudflare.Worker("Worker", {
name: "existing-worker",
main: "./src/worker.ts",
bindings: {
Counter: Cloudflare.DurableObject<Counter>("Counter", {
className: "CounterV2",
}),
},
});

Source: src/Cloudflare/Workers/DurableObjectChatPersistence.ts

A BackingPersistence layer (Effect AI persistence module) backed by the surrounding Durable Object’s state.storage. Drop-in storage for Persistence.layerResultPersisted({ storeId }) so chat history, cached AI responses, or any other persisted state lives in the DO SQLite store with ${storeId}: key namespacing.

Multiple storeIds can coexist within a single Durable Object — keys are namespaced with a ${storeId}: prefix so they don’t collide.

DurableObjectChatPersistence: Wiring it into a chat-backing DO

Section titled “DurableObjectChatPersistence: Wiring it into a chat-backing DO”

Persistence.layerResultPersisted({ storeId }) is the seam Effect AI exposes for cached/replayable AI calls. Layer DurableObjectChatPersistence underneath and every entry is stored in the DO’s state.storage under the alchemy.chat: prefix.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { Chat, LanguageModel } from "effect/unstable/ai";
import { Persistence } from "effect/unstable/persistence";
export default class ChatBackend extends Cloudflare.DurableObject<ChatBackend>()(
"ChatBackend",
Effect.gen(function* () {
return Effect.gen(function* () {
const persistence = yield* Persistence.layerResultPersisted({
storeId: "alchemy.chat",
}).pipe(Layer.provide(Cloudflare.AI.DurableObjectChatPersistence));
return {
send: (threadId: string, prompt: string) =>
LanguageModel.generateText({ prompt }).pipe(Effect.provide(persistence)),
};
});
}),
) {}

DurableObjectChatPersistence: Multiple stores in the same DO

Section titled “DurableObjectChatPersistence: Multiple stores in the same DO”

Different storeIds namespace their keys with ${storeId}:, so one DO can keep, say, chat history and an audit log in separate stores without colliding.

const aiPersistence = yield* Persistence.layerResultPersisted({
storeId: "alchemy.chat",
}).pipe(Layer.provide(Cloudflare.AI.DurableObjectChatPersistence));
const auditPersistence = yield* Persistence.layerResultPersisted({
storeId: "alchemy.audit",
}).pipe(Layer.provide(Cloudflare.AI.DurableObjectChatPersistence));

Source: src/Cloudflare/Workers/EmailEventSource.ts

Subscribe to Cloudflare Email Worker events with an Effect handler.

Wires both halves of the consumer in one call:

  • Runtime: registers an email event listener on the Worker. The handler receives a ForwardableEmailMessage whose action methods (forward, reply, setReject) return Effects.
  • Deploy-time (when zone is set): yields a Cloudflare.Email.Routing toggle on the zone plus the routing resource whose actions: [{ type: "worker", … }] targets this Worker — Cloudflare.Email.CatchAll for a catch-all subscription, Cloudflare.Email.Rule for anything more specific. No manual wiring needed in alchemy.run.ts.

Requires EmailEventSourceLive provided on the Worker’s Effect.

Failure semantics: a failing handler is logged and the failure is re-raised. Cloudflare turns that into a temporary SMTP failure, so the sending server keeps the message and retries later — mail is never accepted and then silently dropped. Handle the failures you consider final inside the handler (Effect.retry, Effect.catchTag, or message.setReject(...) to bounce permanently); anything you let escape becomes a retryable delivery failure.

Catch-all on a zone — auto-creates routing + catch-all

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
export default Cloudflare.Worker(
"Inbox",
{ main: import.meta.url },
Effect.gen(function* () {
yield* Cloudflare.email({ zone: "example.com" }).subscribe(
(message) => message.forward("ops@example.com"),
);
return {};
}).pipe(Effect.provide(Cloudflare.EmailEventSourceLive)),
);

Match a specific address

yield* Cloudflare.email({
zone: "example.com",
matchers: [{ type: "literal", field: "to", value: "hello@example.com" }],
}).subscribe((message) => message.forward("ops@example.com"));

Reject (bounce) a message

yield* Cloudflare.email({ zone: "example.com" }).subscribe((message) =>
message.setReject("Mailbox closed"),
);

Bring-your-own routing — no zone, no auto-create

// Manage `Email.Routing` / `Email.Rule` yourself in alchemy.run.ts.
yield* Cloudflare.email().subscribe((message) =>
Effect.log(`from ${message.from}`),
);

Source: src/Cloudflare/Workers/GitHubRepositoryEventSource.ts

GitHub event source for Cloudflare Workers.

Deploy-time: provisions a Webhook on the repository whose delivery URL points at this Worker (at a deterministic per-repo path). The webhook secret is bound onto the Worker via an Output accessor so the runtime can verify delivery signatures.

Runtime: registers a fetch listener that claims requests on the repository’s delivery path, verifies the HMAC-SHA256 signature against the bound secret, and forwards each delivery to the subscriber. Requests on any other path fall through to the Worker’s own fetch handler.

Source: src/Cloudflare/Workers/ObservabilityDestination.ts

A Workers observability destination — an account-level OTLP export of Workers Logs telemetry (traces, logs, or metrics) pushed to an external HTTPS collector via Logpush.

A destination is identified by its Cloudflare-derived slug (stable, computed from the name at creation). The endpoint URL, headers, and enabled flag are mutable in place; name and logpushDataset force a replacement.

Cloudflare preflights the endpoint with a POST on create (skippable via skipPreflightCheck) and on every in-place update (not skippable), so the collector must answer 2xx for updates to converge.

Safety: destinations carry no ownership markers and Cloudflare enforces one destination per name. When there is no prior state, read scans the account for a destination with the same name and reports it as Unowned, so the engine refuses to take it over unless --adopt (or adopt(true)) is set.

ObservabilityDestination: Exporting Workers traces

Section titled “ObservabilityDestination: Exporting Workers traces”
const traces = yield* Cloudflare.Workers.ObservabilityDestination("Traces", {
url: "https://otel.example.com/v1/traces",
headers: { authorization: secret },
logpushDataset: "opentelemetry-traces",
});

ObservabilityDestination: Exporting Workers logs

Section titled “ObservabilityDestination: Exporting Workers logs”
const logs = yield* Cloudflare.Workers.ObservabilityDestination("Logs", {
name: "my-app-logs",
url: "https://collector.example.com/v1/logs",
logpushDataset: "opentelemetry-logs",
skipPreflightCheck: true,
});

ObservabilityDestination: Pausing an export

Section titled “ObservabilityDestination: Pausing an export”
yield* Cloudflare.Workers.ObservabilityDestination("Logs", {
name: "my-app-logs",
url: "https://collector.example.com/v1/logs",
logpushDataset: "opentelemetry-logs",
enabled: false,
});

Source: src/Cloudflare/Workers/RateLimit.ts

A Cloudflare Rate Limit binding for counting arbitrary keys inside Workers — a Worker-only binding with no backing cloud resource.

RateLimit is a single value that is at once the Binding.Service tag, the callable that produces a RateLimitBinding, and the type. Declare it on a Worker’s env (it flows through InferEnv → the native cf.RateLimit) or yield* it inside an Effect-native Worker to attach the binding and obtain the RateLimitClient.

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: {
THROTTLE: Cloudflare.RateLimit("THROTTLE", {
namespaceId: 1001,
simple: { limit: 10, period: 60 },
}),
},
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { THROTTLE: RateLimit } — the native Cloudflare binding
// worker.ts
export default {
fetch: async (req: Request, env: WorkerEnv) => {
const { success } = await env.THROTTLE.limit({ key: "ip" });
return new Response(success ? "ok" : "rate limited");
},
};

RateLimit: Binding inside an Effect-native Worker

Section titled “RateLimit: Binding inside an Effect-native Worker”
Cloudflare.Worker("Worker", { main: "./src/worker.ts" },
Effect.gen(function* () {
// Attaches the binding to this Worker AND returns the runtime client.
const throttle = yield* Cloudflare.RateLimit("THROTTLE", {
namespaceId: 1001,
simple: { limit: 10, period: 60 },
});
return {
fetch: Effect.gen(function* () {
const { success } = yield* throttle.limit({ key: "ip" });
return HttpServerResponse.text(success ? "ok" : "rate limited");
}),
};
}).pipe(Effect.provide(Cloudflare.Workers.RateLimitBinding)),
);

Source: src/Cloudflare/Workers/Route.ts

A Workers Route — a zone-level mapping from a URL pattern to a Worker script.

Routes are the classic way to serve a Worker on a zone hostname or path. The matched hostname must resolve through Cloudflare’s proxy, so pair the route with a proxied DNS record (an AAAA 100:: placeholder is the conventional choice when the Worker is the only origin).

Safety: routes carry no ownership markers, and Cloudflare enforces one route per pattern per zone. When there is no prior state, read scans the zone for an existing route with the same pattern and reports it as Unowned, so the engine refuses to take it over unless --adopt (or adopt(true)) is set.

WorkerRoute: Routing a hostname to a Worker

Section titled “WorkerRoute: Routing a hostname to a Worker”
const worker = yield* Cloudflare.Worker("Api", {
main: "./src/api.ts",
});
yield* Cloudflare.Workers.WorkerRoute("ApiRoute", {
zoneId: zone.zoneId,
pattern: "api.example.com/*",
script: worker.workerName,
});
// Workers only run on proxied hostnames — give the host an origin.
yield* Cloudflare.DNS.Record("ApiPlaceholder", {
zoneId: zone.zoneId,
name: "api.example.com",
type: "AAAA",
content: "100::",
proxied: true,
});
// No `script` — matching requests bypass Workers entirely.
yield* Cloudflare.Workers.WorkerRoute("AssetsBypass", {
zoneId: zone.zoneId,
pattern: "example.com/assets/*",
});

Source: src/Cloudflare/Workers/RpcDurableObject.ts

RpcDurableObject is sugar over DurableObject for Durable Objects whose surface is a typed Effect RpcGroup. The inner Effect returns the group’s handler Layer, automatically enabling HTTP RPC with NDJSON and hibernating WebSocket RPC with JSON. Incoming requests select the transport. Consumers see namespace.getByName(id) as a typed HTTP RpcClient. Existing implementations returning RpcServer.toHttpEffect(group) remain supported for HTTP.

Use this over alchemy’s built-in DO method bridge whenever values crossing the DO boundary contain Schema.Class instances. The built-in bridge JSON.stringifys every method return value, which strips class identity (e.g. an effect/ai Response.Usage instance becomes a plain struct on the consumer side). With RpcDurableObject, both ends go through the same RpcSerialization codec, so Schema.decode reconstructs class instances correctly.

The DO instance is the session, so the group payloads typically don’t include any per-session identifier — only the per-call inputs.

import * as Schema from "effect/Schema";
import { Rpc, RpcGroup } from "effect/unstable/rpc";
const setTitle = Rpc.make("setTitle", {
success: Schema.Void,
payload: { title: Schema.String },
});
const getTitle = Rpc.make("getTitle", {
success: Schema.String,
payload: {},
});
export class CounterRpcs extends RpcGroup.make(setTitle, getTitle) {}

RpcDurableObject: Implementing the Durable Object

Section titled “RpcDurableObject: Implementing the Durable Object”

Mirrors Cloudflare.DurableObject<Self>()(...) — same outer/inner Effect pattern. The outer Effect resolves shared deps; the per-instance inner Effect returns the RPC handler Layer.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { CounterRpcs } from "./rpcs.ts";
export default class Counter extends Cloudflare.RpcDurableObject<Counter>()(
"Counter",
{ schema: CounterRpcs },
Effect.gen(function* () {
// outer init: shared deps + the instance state reference
const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
// inner (runtime): state.storage is RuntimeContext-colored, so
// the handler closures that call it live here
return CounterRpcs.toLayer({
setTitle: ({ title }) => state.storage.put("title", title),
getTitle: () =>
Effect.map(state.storage.get<string>("title"), (t) => t ?? ""),
});
});
}),
) {}

RpcDurableObject: Calling the DO from a Worker

Section titled “RpcDurableObject: Calling the DO from a Worker”

yield* Counter resolves to a value whose getByName(id) returns an Effect<RpcClient<CounterRpcs>>. Each rpc method is a typed Effect/Stream factory — no RpcClient.make setup needed. Yield the client inside a per-request Effect.scoped handler so it’s freed with the request.

import Counter from "./counter.ts";
Effect.gen(function* () {
const counters = yield* Counter;
const client = yield* counters.getByName("global");
yield* client.setTitle({ title: "Hello" });
const title = yield* client.getTitle({});
return title;
}).pipe(Effect.scoped);

Handler-Layer implementations accept WebSocket upgrades automatically; ordinary HTTP RPC remains available on the same object.

import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
const counters = yield* Counter;
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const path = new URL(request.url, "https://worker").pathname;
const name = /^\/counters\/([a-zA-Z0-9_-]+)$/.exec(path)?.[1];
if (!name) return HttpServerResponse.empty({ status: 404 });
return yield* counters.fetch(name, request);
}),
};

A client connecting to wss://example.com/counters/alice targets the "alice" instance. Every RPC on that socket stays on that object; RPC payloads do not need an object ID. Another name selects another object. The Worker defines this URL mapping, not Alchemy. Authenticate and authorize access to the selected name before forwarding. See the Effect RPC guide for a browser client whose Layer owns the connection lifetime. Clients use Effect’s RpcClient.layerProtocolSocket with JSON serialization. Idle connections survive hibernation. Restored sockets with unfinished requests close with code 1012; platform resets can also cause transport errors. Requests and streams are never replayed. No application acknowledgment methods or checkpoints are required.

RpcDurableObject: Modular form: separate the class from its runtime

Section titled “RpcDurableObject: Modular form: separate the class from its runtime”

The inline class form above bundles the runtime into the class declaration. The two-arg form (name, { schema }) declares the class as a pure tagged identifier; provide the runtime separately via Class.make(impl). Consumer Workers can import the class for binding (Counter.from(HostWorker)) without pulling the runtime into their bundle.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { CounterRpcs } from "./rpcs.ts";
export class Counter extends Cloudflare.RpcDurableObject<Counter>()(
"Counter",
{ schema: CounterRpcs },
) {}
// Only the host script imports this default export.
export default Counter.make(
Effect.gen(function* () {
const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () {
return CounterRpcs.toLayer({
setTitle: ({ title }) => state.storage.put("title", title),
getTitle: () =>
Effect.map(state.storage.get<string>("title"), (t) => t ?? ""),
});
});
}),
);

RpcDurableObject: Cross-script binding via Counter.from(Worker)

Section titled “RpcDurableObject: Cross-script binding via Counter.from(Worker)”

Hosting on WorkerA, binding from WorkerB

The host Worker declares Counter in its Deps (third type arg of Worker<Self, Bindings, Deps> or second of RpcWorker<Self, Deps>) and provides CounterLive. Any other Worker uses Counter.from(HostWorker) to bind to the same DO instances — writes through HostWorker.getByName(name) are visible from Counter.from(HostWorker).getByName(name).

// host worker (declares + provides Counter)
import CounterLive, { Counter } from "./counter.ts";
export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()(
"WorkerA",
{ main: import.meta.url },
) {}
export default WorkerA.make(
Effect.gen(function* () {
const counters = yield* Counter; // local host binding
// ... fetch handler ...
}).pipe(Effect.provide(CounterLive)),
);
// consumer worker (binds via .from)
export default class WorkerB extends Cloudflare.Worker<WorkerB>()(
"WorkerB",
{ main: import.meta.url },
Effect.gen(function* () {
const counters = yield* Counter.from(WorkerA);
return {
fetch: Effect.gen(function* () {
const client = yield* counters.getByName("shared");
yield* client.setTitle({ title: "via WorkerB" });
return HttpServerResponse.text("ok");
}).pipe(Effect.scoped),
};
}),
) {}

Self-hosted isolated namespace

A Worker that declares Counter in its own Deps and provides CounterLive hosts its own isolated namespace — instances under it are separate from any other host’s. Use Counter.from(Self) inside the host to be explicit about which script’s namespace you’re binding to.

export class WorkerC extends Cloudflare.Worker<WorkerC, {}, Counter>()(
"WorkerC",
{ main: import.meta.url },
) {}
export default WorkerC.make(
Effect.gen(function* () {
const counters = yield* Counter.from(WorkerC); // explicit self
// ... fetch handler ...
}).pipe(Effect.provide(CounterLive)),
);

RpcDurableObject: Yielding the surrounding namespace from inside a DO

Section titled “RpcDurableObject: Yielding the surrounding namespace from inside a DO”

Lets a DO instance refer to its own namespace — e.g. to fan a call out to sibling instances. Mirrors yield* DurableObject on the regular DurableObject.

Effect.gen(function* () {
const self = yield* Cloudflare.RpcDurableObject;
const peer = yield* self.getByName("peer-1");
yield* peer.setTitle({ title: "Sibling call" });
}).pipe(Effect.scoped);

Source: src/Cloudflare/Workers/RpcWebSocketClient.ts Kind: Layer · Provides: service

Provide a typed RPC client and its WebSocket connection for the Layer’s lifetime.

Import from alchemy/Cloudflare/RpcWebSocketClient in browsers to avoid Cloudflare server modules. Provide Socket.layerWebSocketConstructorGlobal in a browser, or the corresponding constructor Layer for another platform. RPC client middleware remains an explicit dependency.

Provide this Layer around the application or session that shares the client. Releasing the Layer closes the connection on success, failure, or interruption. Ordinary calls need no caller Scope; streaming calls using { asQueue: true } still require a Scope for the queue consumer. Inside a Cloudflare Worker, provide this Layer per request, not in the isolate-scoped initializer.

import * as RpcWebSocketClient from "alchemy/Cloudflare/RpcWebSocketClient";
import { Context, Effect, Layer } from "effect";
import type * as RpcClient from "effect/unstable/rpc/RpcClient";
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
import * as Socket from "effect/unstable/socket/Socket";
import { CounterRpcs } from "./rpcs.ts";
class CounterClient extends Context.Service<
CounterClient,
RpcClient.FromGroup<typeof CounterRpcs, RpcClientError>
>()("CounterClient") {}
const ClientLive = RpcWebSocketClient.layer(
CounterClient,
CounterRpcs,
"wss://example.com/counters/alice",
).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
const program = Effect.gen(function* () {
const counter = yield* CounterClient;
return yield* counter.increment();
}).pipe(Effect.provide(ClientLive));

Source: src/Cloudflare/Workers/RpcWorker.ts

RpcWorker is a thin sugar over Worker for the common case where a worker’s entire fetch surface is a typed Effect RpcGroup. It takes the rpc schema directly in props alongside main, and accepts an init Effect that returns the already-piped RpcServer.toHttpEffect(...)-producing Effect (no { fetch } wrapper) — the wrapper plugs it into the worker’s fetch for you.

Functionally identical to writing Cloudflare.Worker(...) with return { fetch: RpcServer.toHttpEffect(schema).pipe(...) }; use whichever style you prefer.

The class form (class X extends Cloudflare.RpcWorker<X>()(...)) carries Self through the result type as Rpc<Self>, so other workers binding to this one see the rpc shape pinned to Self.

The rpc group and its schemas live outside any worker so both the server (RpcWorker) and any consumers (RpcClient.make / RpcDurableObject) import the same value.

import * as Schema from "effect/Schema";
import { Rpc, RpcGroup } from "effect/unstable/rpc";
export class TaskNotFound extends Schema.TaggedClass<TaskNotFound>()(
"TaskNotFound",
{ id: Schema.String },
) {}
const getTask = Rpc.make("getTask", {
payload: { id: Schema.String },
success: Schema.String,
error: TaskNotFound,
});
export class TaskRpcs extends RpcGroup.make(getTask) {}

Class form (recommended)

Mirrors Cloudflare.Worker<Self>()(...)class X extends ... works the same. The init Effect builds a handlers Layer from the group and returns the RpcServer.toHttpEffect(schema)-piped Effect directly.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
import { TaskRpcs } from "./rpcs.ts";
export default class Worker extends Cloudflare.RpcWorker<Worker>()(
"Worker",
{ main: import.meta.url, schema: TaskRpcs },
Effect.gen(function* () {
const handlers = TaskRpcs.toLayer({
getTask: ({ id }) => Effect.succeed(`task-${id}`),
});
return RpcServer.toHttpEffect(TaskRpcs).pipe(
Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerJson)),
);
}),
) {}

NDJSON for streaming rpcs

If any rpc in the group is a streaming rpc, the wire serialization must be RpcSerialization.layerNdjson — streaming rpcs need newline framing on the wire.

return RpcServer.toHttpEffect(ChatRpcs).pipe(
Effect.provide(handlers),
Effect.provide(RpcSerialization.layerNdjson),
);

RpcWorker: Modular form: separate the class from its runtime

Section titled “RpcWorker: Modular form: separate the class from its runtime”

The inline class form above bundles the runtime into the class declaration. The two-arg form (id, props) declares the class as a pure tagged identifier; provide the runtime separately via WorkerClass.make(impl) so consumers can import the class for binding without pulling the host’s runtime into their bundle.

export class TaskWorker extends Cloudflare.RpcWorker<TaskWorker>()(
"TaskWorker",
{ main: import.meta.url, schema: TaskRpcs },
) {}
// Only the host script imports this default export; consumers
// import the class above for `RpcWorker.bind(TaskWorker)`.
export default TaskWorker.make(
Effect.gen(function* () {
const handlers = TaskRpcs.toLayer({
getTask: ({ id }) => Effect.succeed(`task-${id}`),
});
return RpcServer.toHttpEffect(TaskRpcs).pipe(
Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerJson)),
);
}),
);

RpcWorker: Hosting a Durable Object for cross-script binding

Section titled “RpcWorker: Hosting a Durable Object for cross-script binding”

The optional second type argument Deps mirrors Cloudflare.Worker<Self, Bindings, Deps> — it declares the DOs this Worker publishes for cross-script binding. With Counter named in Deps, any other Worker can write Counter.from(TaskWorker) and have it type-check.

import { Counter } from "./counter.ts";
export class TaskWorker extends Cloudflare.RpcWorker<TaskWorker, Counter>()(
"TaskWorker",
{ main: import.meta.url, schema: TaskRpcs },
) {}

See RpcDurableObject for the consumer side (Counter.from(TaskWorker)).

Inside another worker’s init, RpcWorker.bind(WorkerClass) registers the service binding on the surrounding worker and returns a typed RpcClient you can call directly from any per-request handler. Internally each method invocation builds a fresh underlying client (because Cloudflare rejects cross-request reuse of the stub I/O), but that’s hidden behind a Proxy so the consumer sees a normal RpcClient.

import TaskWorker from "./task-worker.ts";
export default class Caller extends Cloudflare.RpcWorker<Caller>()(
"Caller",
{ main: import.meta.url, schema: CallerRpcs },
Effect.gen(function* () {
// INIT: register binding, get the typed client
const tasks = yield* Cloudflare.RpcWorker.bind(TaskWorker);
const handlers = CallerRpcs.toLayer({
// PER-REQUEST: just call methods directly
proxyGetTask: ({ id }) => tasks.getTask({ id }),
});
return RpcServer.toHttpEffect(CallerRpcs).pipe(
Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerJson)),
);
}),
) {}

The same RpcGroup drives a typed client. Test.make deploys the stack once for the file; each test yields the deploy handle for its URL and calls procedures directly.

import { expect } from "alchemy-test";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Test from "alchemy/Test/Alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schedule from "effect/Schedule";
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
import * as RpcClient from "effect/unstable/rpc/RpcClient";
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
import Stack from "../alchemy.run.ts";
import { TaskRpcs } from "../src/rpcs.ts";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers: Cloudflare.providers(),
});
const stack = beforeAll(deploy(Stack));
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
const layer = (url: string) =>
RpcClient.layerProtocolHttp({ url }).pipe(
Layer.provide(FetchHttpClient.layer),
Layer.provide(
Layer.succeed(RpcSerialization.RpcSerialization, RpcSerialization.json),
),
);
test(
"getTask",
Effect.gen(function* () {
const { url } = yield* stack;
yield* Effect.gen(function* () {
const client = yield* RpcClient.make(TaskRpcs);
const result = yield* client
.getTask({ id: "abc" })
.pipe(Effect.retry({ schedule: Schedule.exponential("500 millis"), times: 5 }));
expect(result).toBe("task-abc");
}).pipe(Effect.scoped, Effect.provide(layer(url)));
}),
);

RpcWorker: Yielding the surrounding worker from inside the impl

Section titled “RpcWorker: Yielding the surrounding worker from inside the impl”

Mirrors yield* DurableObject — yield the tag to access the surrounding worker.

Effect.gen(function* () {
const self = yield* Cloudflare.RpcWorker;
});

Source: src/Cloudflare/Workers/SecretKey.ts

A Cloudflare Workers Secret Key binding — key material uploaded once and exposed to the Worker as a native, non-extractable CryptoKey. The Worker can sign, verify, encrypt, or decrypt with the key via crypto.subtle, but can never read the raw key material back out.

SecretKey is a single value that is at once the Binding.Service tag, the callable that produces a SecretKeyBinding, and the type. Declare it on a Worker’s env (it flows through InferEnv → the native CryptoKey) or yield* it inside an Effect-native Worker to attach the binding and obtain a deferred SecretKeyAccessor.

SecretKey: Binding inside an Effect-native Worker

Section titled “SecretKey: Binding inside an Effect-native Worker”

HMAC sign and verify

Cloudflare.Worker(
"SignerWorker",
{ main: import.meta.url },
Effect.gen(function* () {
// Attaches the binding to this Worker AND returns a deferred accessor.
const hmacKey = yield* Cloudflare.Workers.SecretKey("HMAC_KEY", {
format: "raw",
algorithm: { name: "HMAC", hash: "SHA-256" },
usages: ["sign", "verify"],
keyBase64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
});
return {
fetch: Effect.gen(function* () {
const key = yield* hmacKey;
const data = new TextEncoder().encode("hello");
const signature = yield* Effect.promise(() =>
crypto.subtle.sign("HMAC", key, data),
);
const valid = yield* Effect.promise(() =>
crypto.subtle.verify("HMAC", key, signature, data),
);
return HttpServerResponse.json({ valid });
}),
};
}).pipe(Effect.provide(Cloudflare.Workers.SecretKeyBinding)),
);

JSON Web Key format

const jwkKey = yield* Cloudflare.Workers.SecretKey("HMAC_KEY_JWK", {
format: "jwk",
algorithm: { name: "HMAC", hash: "SHA-256" },
usages: ["sign", "verify"],
keyJwk: {
kty: "oct",
k: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
alg: "HS256",
},
});
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: {
HMAC_KEY: Cloudflare.Workers.SecretKey("HMAC_KEY", {
format: "raw",
algorithm: { name: "HMAC", hash: "SHA-256" },
usages: ["sign", "verify"],
keyBase64: Redacted.make("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="),
}),
},
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { HMAC_KEY: CryptoKey } — the native runtime binding
// worker.ts
export default {
fetch: async (req: Request, env: WorkerEnv) => {
const signature = await crypto.subtle.sign(
"HMAC",
env.HMAC_KEY,
new TextEncoder().encode("hello"),
);
return new Response(btoa(String.fromCharCode(...new Uint8Array(signature))));
},
};

Source: src/Cloudflare/Workers/SqlMigrations.ts

Read SQL migrations during construction and carry them into a Durable Object without importing .sql files or generated migrations.js.

Files are ordered and hashed using Alchemy’s shared SQL migration format. Flat SQL files and modern drizzle-kit migration directories are supported. Paths are relative to the directory where Alchemy runs, not this module. The records are embedded in the Worker JavaScript bundle, so they do not consume environment-variable bindings. The runtime never reads the directory. Missing directories and unsupported layouts fail construction.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Drizzle from "alchemy/Drizzle/Cloudflare";
import * as Effect from "effect/Effect";
import { relations, users } from "./schema.ts";
export class Users extends Cloudflare.DurableObject<Users>()(
"Users",
Effect.gen(function* () {
const migrations = yield* Cloudflare.SqlMigrations("./drizzle");
return Effect.gen(function* () {
const db = yield* Drizzle.DurableObject({ migrations, relations });
return { list: () => db.select().from(users) };
});
}),
) {}

SqlMigrations: Apply Migrations Without Drizzle

Section titled “SqlMigrations: Apply Migrations Without Drizzle”
Effect.gen(function* () {
const migrations = yield* Cloudflare.SqlMigrations("./drizzle");
return Effect.gen(function* () {
yield* migrations.apply().pipe(Effect.orDie);
return {};
});
});

apply() requires RuntimeContext and the current Durable Object state. Each pending file and its __alchemy_migrations row commit atomically. A failed file rolls back; successfully applied earlier files stay committed. Existing modern Drizzle history is adopted without replaying applied SQL.

const migrations = yield* Cloudflare.SqlMigrations({
dir: "./drizzle",
table: "app_migrations",
});

Source: src/Cloudflare/Workers/Subdomain.ts

The account-wide workers.dev subdomain singleton (/accounts/{account_id}/workers/subdomain). Every Worker with workers.dev enabled is served at https://<script>.<subdomain>.workers.dev.

Each account has at most one subdomain — “creating” this resource claims (or renames to) the requested name. Subdomain names are globally unique across all Cloudflare accounts; claiming a taken name fails with the typed SubdomainAlreadyExists error.

Destroy is capture-and-restore: the subdomain is renamed back to the value it had before Alchemy first managed it. If the account had no subdomain at first touch, destroy removes it entirely.

Warning: renaming or removing the subdomain immediately changes the URL of every deployed Worker on the account that relies on workers.dev. Only manage this resource on accounts where that is acceptable.

const sub = yield* Cloudflare.Workers.Subdomain("Subdomain", {
subdomain: "my-team",
});
// Workers are now served from https://<script>.my-team.workers.dev

Source: src/Cloudflare/Workers/Telemetry.ts Kind: Layer · Provides: Tracer.Tracer

Mirror Effect spans into Cloudflare Workers Observability.

A binding layer: at deploy time it turns on observability.traces on the host Worker (the same path as Cloudflare.cache()) and registers a per-event Effect Tracer. Cloudflare auto-instruments fetch/KV/R2/D1; Effect.withSpan / Effect.fn frames nest in that waterfall. Cloudflare owns sampling and export — no OTLP URL, flush, or Wrangler block.

Until the global default compatibility date is raised past 2026-07-28, pin compatibility: { date: "2026-08-25" } (or later) or deploy fails. alchemy dev does not gate the date: what a local Worker records is the local runtime’s concern.

Compose it into the Function/Worker’s single Effect.provide:

export default Cloudflare.Worker(
"Worker",
{
main: import.meta.url,
compatibility: { date: "2026-08-25" },
},
Effect.gen(function* () {
return {
fetch: Effect.gen(function* () {
yield* doWork().pipe(Effect.withSpan("operation"));
return HttpServerResponse.text("ok");
}),
};
}).pipe(
Effect.provide(
Layer.mergeAll(
Cloudflare.R2.ReadWriteBucketBinding,
Cloudflare.Telemetry(),
),
),
),
);
Effect.provide(
Layer.mergeAll(
Cloudflare.Telemetry(),
// omit `traces` — Cloudflare.Telemetry provides the Tracer
Axiom.Telemetry({ token: Ingest, logs: Logs, metrics: Metrics }),
),
)

To ship the same waterfall (Effect + platform spans) elsewhere, export it from Cloudflare with Cloudflare.Workers.ObservabilityDestination rather than sending a second Worker-side OTLP trace stream.

Source: src/Cloudflare/Workers/VersionMetadata.ts

A Cloudflare Workers Version Metadata binding — a Worker-only binding with no backing cloud resource. Cloudflare provides the deployed Worker version at runtime (id, tag, timestamp).

VersionMetadata is a single value that is at once the Binding.Service tag, the callable that produces a VersionMetadataBinding, and the type. Declare it on a Worker’s env (it flows through InferEnvWorkerVersionMetadata) or yield* it inside an Effect-native Worker to attach the binding and obtain a deferred VersionMetadataAccessor.

Section titled “VersionMetadata: Effect-style Worker (recommended)”
import * as Effect from "effect/Effect";
Cloudflare.Worker(
"VersionWorker",
{ main: import.meta.url },
Effect.gen(function* () {
// Attaches the binding to this Worker AND returns a deferred accessor.
const versionMetadata = yield* Cloudflare.Workers.VersionMetadata();
return {
fetch: Effect.gen(function* () {
const { id, tag, timestamp } = yield* versionMetadata;
return Response.json({ id, tag, timestamp });
}),
};
}).pipe(Effect.provide(Cloudflare.Workers.VersionMetadataBinding)),
);
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { CF_VERSION_METADATA: Cloudflare.Workers.VersionMetadata() },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { CF_VERSION_METADATA: WorkerVersionMetadata }

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* () {
// Construction: bind resources (deploy time and cold start)
const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return {
// Runtime: handlers, once per 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 Runtime page for the full explanation.

  • Async — plain async fetch handler, 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.

Put Cloudflare Access in front of the Worker with the access prop — unauthenticated requests are redirected to your team’s login page, and handlers read the authenticated identity from ctx.access via Cloudflare.Access.Context. See the Protect a Worker with Access guide.

Dedicated application — per-Worker policies

The { policies } form declares an Access application owned by this Worker (namespaced under it as <Worker>/Access), created, updated, and deleted with it:

export default Cloudflare.Worker(
"Api",
{
main: import.meta.url,
access: {
policies: [
{ decision: "allow", include: [{ emailDomain: "example.com" }] },
],
},
// simulate the authenticated state under `alchemy dev`
dev: { access: { aud: "dev", identity: { email: "dev@example.com" } } },
},
Effect.gen(function* () {
return {
fetch: Effect.gen(function* () {
const access = yield* Cloudflare.Access.Context;
const identity = yield* access!.getIdentity();
return yield* HttpServerResponse.json({ email: identity?.email });
}),
};
}),
);

Shared application — one policy set, many Workers

Pass a Cloudflare.Access.Application directly to enroll into it. Access policies are application-wide: every enrolled Worker is gated by the same policy set.

const TeamOnly = Cloudflare.Access.Application("TeamOnly", {
type: "self_hosted",
policies: [
{ decision: "allow", include: [{ emailDomain: "example.com" }] },
],
});
export default Cloudflare.Worker(
"Api",
{ main: import.meta.url, access: TeamOnly },
/* ... *​/
);

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

alchemy.run.ts
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

src/worker.ts
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 });
},
};

Binding a named entrypoint

env: { TARGET: worker } targets the Worker’s default entrypoint. Cloudflare.WorkerEntrypoint binds one of its named WorkerEntrypoint class exports instead; InferEnv types the entry as a Fetcher stub whose RPC methods are called directly.

const caller = yield* Cloudflare.Worker("Caller", {
main: "./src/caller.ts",
env: {
API: Cloudflare.WorkerEntrypoint(target, "Api"), // env.API.greet("alice")
},
});

Entrypoint props

The options form attaches properties the target entrypoint reads from this.ctx.props. Output values resolve at deploy time. alchemy dev delivers props to the local workerd; the deploy API’s binding schema does not carry the field yet, so props are dropped at upload while the binding itself deploys correctly.

env: {
VENDOR: Cloudflare.WorkerEntrypoint(vendorWorker, {
entrypoint: "Vendor",
props: { baseUrl: site.url },
}),
}

Point main at a .py file to deploy a Python Worker (open beta). There is no bundling step — the entry and every sibling .py module upload as-is and are interpreted by Pyodide, and the python_workers compatibility flag is added automatically. Like async Workers, Python Workers take no inline Effect implementation; declare bindings with the env prop and read them from self.env in Python.

Dependencies come from pyproject.toml next to the entry: Alchemy vendors [project.dependencies] with uv against the Pyodide wheel index and uploads them under python_modules/. If a python_modules/ directory already exists (e.g. produced by pywrangler sync), it is uploaded as-is and uv is not invoked.

See the Python Workers guide for the full walkthrough.

Defining a Python Worker in your stack

alchemy.run.ts
const kv = yield* Cloudflare.KV.Namespace("Cache");
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.py",
env: { CACHE: kv },
});

Writing the Python handler

src/worker.py
from workers import Response, WorkerEntrypoint
class Default(WorkerEntrypoint):
async def fetch(self, request):
cached = await self.env.CACHE.get("greeting")
return Response(cached or "Hello from Python!")

Vendoring dependencies with pyproject.toml

# src/pyproject.toml — vendored with uv on deploy
[project]
name = "my-worker"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = ["humanize"]

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* () {
// Construction: 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");
}),
};
}),
) {}

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* () {
// Construction: 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");
}),
};
}),
) {}

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-08-31",
},
}

Serving static assets

{
main: import.meta.url,
assets: "./public",
}

Run the Worker before the asset layer

A path that matches no asset already falls through to the Worker. runWorkerFirst routes the listed paths (or, with true, every request) through the Worker ahead of asset matching, for an asset that would otherwise shadow a route or an SPA fallback that would answer an API call. A _headers or _redirects file in the directory is applied automatically and .assetsignore excludes files from the upload.

{
main: import.meta.url,
assets: {
directory: "./public",
runWorkerFirst: ["/api/*", "/admin/*"],
},
}

Assets-only Worker (static site)

Omit main and script entirely to deploy a static site: no Worker code is uploaded — Cloudflare’s asset layer serves every request and applies htmlHandling / notFoundHandling (including SPA fallback) itself, exactly like an assets-only wrangler deploy.

const site = yield* Cloudflare.Worker("Site", {
assets: {
directory: "./public",
htmlHandling: "drop-trailing-slash",
notFoundHandling: "404-page",
},
domain: "static.example.com",
});

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",
}

main is bundled with rolldown at deploy time. Unused code is tree-shaken. effect, alchemy, and @distilled.cloud are marked pure so unused parts prune more aggressively. Your app is not marked pure.

Mark additional packages as pure

Only list packages with no top-level side effects.

{
main: "./src/worker.ts",
build: {
pure: { packages: ["my-lib", "@my-scope/*"] },
},
}

Replace Node modules with Worker-compatible stubs

Use Rolldown’s build.input.resolve.alias for module replacements. Aliases apply to imports and static require() calls before Node compatibility shims. Use absolute paths for file replacements. Keep bundling enabled: bundle: false uploads files unchanged and does not apply aliases. Alternatively, apply aliases in your external build before uploading its output with bundle: false.

import * as Path from "effect/Path";
const path = yield* Path.Path;
const stub = yield* path.fromFileUrl(
new URL("./.mastra/output/module-stub.mjs", import.meta.url),
);
const worker = yield* Cloudflare.Worker("Worker", {
main: "./.mastra/output/index.mjs",
compatibility: {
date: "2025-04-01",
flags: ["nodejs_compat", "nodejs_compat_populate_process_env"],
},
build: {
input: { resolve: { alias: { module: stub, "node:module": stub } } },
},
});

Turn it off

{
main: "./src/worker.ts",
build: { pure: false },
}

Every URL that serves the Worker is collected in worker.urls, most significant first, and worker.url is always urls[0]. The ranking: the canonical custom domain (domain.name), then aliases in declared order, then the stable workers.dev URL, then version preview URLs. Under alchemy dev, urls is the local dev server’s [localhost, ...LAN] addresses instead. Redirect hostnames never appear in urls — they serve no content.

The workersDev prop controls the workers.dev surface (true by default = stable URL + version previews; false = neither; object form toggles independently), and the domain prop attaches custom domains — DNS records and edge certificates are managed automatically.

Custom domain with aliases and redirects

const worker = yield* Cloudflare.Worker("Api", {
main: "./src/api.ts",
domain: {
name: "example.com",
zoneId: "<YOUR_ZONE_ID>",
aliases: ["www.example.com"],
redirects: ["old.example.com"], // 301 → https://example.com
},
});
// worker.url === "https://example.com"
// worker.urls === ["https://example.com", "https://www.example.com",
// "https://<name>.<account>.workers.dev"]

workers.dev toggles

// No workers.dev URLs at all:
{ main: "./src/api.ts", workersDev: false, domain: "api.example.com" }
// Previews only — each deploy's version preview URL becomes worker.url:
{ main: "./src/api.ts", workersDev: { enabled: false, previewsEnabled: true } }

All URLs as a CORS allow-list

const site = yield* Cloudflare.Worker("Site", {
main: "./src/site.ts",
domain: { name: "example.com", aliases: ["www.example.com"] },
});
const api = yield* Cloudflare.Worker("Api", {
main: "./src/api.ts",
env: { ALLOWED_ORIGINS: site.urls },
});

The preview prop maps Cloudflare’s Worker Previews — a named copy of a Worker with its own URL, variables, secrets, bindings, and isolated same-Worker Durable Object state. Use it for branch and pull-request testing. Distinct from version: a version is an immutable upload onto the parent script (gradual rollouts, canaries); a Preview does not take production traffic.

A Preview Worker’s url is its stable Preview URL (<name>-<parent>.<subdomain>.workers.dev, or <name>.<domain> when the parent has domain.previews). The name defaults to the stack stage (override with preview.name). Destroying the Preview Worker deletes the Preview; the parent is untouched.

PR preview of another stage’s Worker

const parent = yield* Cloudflare.Worker.ref("Api", { stage: "prod" });
const preview = yield* Cloudflare.Worker("Api", {
main: "./src/api.ts",
preview: { of: parent, message: `PR #${process.env.PR_NUMBER}` },
});
// preview.url -> https://<stage>-<name>.<subdomain>.workers.dev

Custom-domain Preview URLs

// On the production Worker:
yield* Cloudflare.Worker("Api", {
main: "./src/api.ts",
domain: { name: "app.example.com", previews: true },
});
// A Preview of that Worker is then at https://<preview-name>.app.example.com

The version prop maps Cloudflare’s versions and gradual deployments onto Alchemy stages. A Worker with version.parent set uploads an immutable version to the parent Worker’s script instead of creating its own — give it traffic to run it as a canary, or use version.traffic on a normal Worker to roll out its own deploys gradually. For branch and pull-request testing, use preview instead.

A version worker’s url is its aliased preview URL (<alias>-<name>.<subdomain>.workers.dev) — the alias is derived from the stack, stage, and logical id (override with version.alias), so the URL is stable across deploys and always points at the latest uploaded version. The per-version URL (<version-prefix>-...) is also returned in domains. Because the aliased URL is known before the version exists, Worker.URL works on version workers and resolves to it.

A version carries code, static assets, bindings, and compatibility settings. Script-level settings (routes, domains, crons, tags, observability, …) belong to the parent and are rejected on version workers, as are locally-hosted Durable Object or Workflow classes. Preview URLs require the parent’s workers.dev subdomain to be enabled (the default).

Upload a version without routing traffic

// Inspect this upload at its Version URL before a gradual rollout.
// For branch/PR testing, use `preview.of` instead.
yield* Cloudflare.Worker("MyWorker", {
main: "./src/worker.ts",
version: { traffic: 0, tag: process.env.GITHUB_SHA },
});

Canary: send 10% of the parent’s traffic to a version

const parent = yield* Cloudflare.Worker.ref("MyWorker", { stage: "prod" });
yield* Cloudflare.Worker("MyWorker", {
main: "./src/worker.ts",
version: { parent, traffic: 10 },
});

Gradual rollout of a Worker’s own deploy

// The new version takes 25% of traffic; the previously-live version
// keeps 75%. Bump traffic (or remove the prop) and re-deploy to promote.
yield* Cloudflare.Worker("MyWorker", {
main: "./src/worker.ts",
version: { traffic: 25 },
});

Keep users on one version during the rollout

// Percentages route each request independently; affinity pins users by
// filling the Cloudflare-Workers-Version-Key header on zone traffic —
// here from the session cookie, falling back to the client IP. Requires
// a `domain` or `routes` (with `parent`, the parent's).
yield* Cloudflare.Worker("MyWorker", {
main: "./src/worker.ts",
domain: "api.example.com",
version: {
traffic: 25,
affinity: { cookie: "session_id", ip: true },
},
});

Worker.URL injects the URL a Worker is served at as a binding on that same Worker — the first custom domain if one is configured, otherwise its workers.dev URL, always equal to the resource’s url attribute. Under alchemy dev it resolves to the local dev server’s URL.

Read the Worker’s own URL inside a handler

Cloudflare.Worker(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
// Attaches the binding and returns a deferred accessor.
const url = yield* Cloudflare.Worker.URL;
return {
fetch: Effect.gen(function* () {
const publicUrl = yield* url;
return yield* HttpServerResponse.json({ url: publicUrl });
}),
};
}),
);

Inject the URL into an async Worker’s env

InferEnv types the entry as string. A VITE_-prefixed key on a vite-built Worker is additionally inlined into the client bundle as import.meta.env.VITE_PUBLIC_URL at build time.

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { PUBLIC_URL: Cloudflare.Worker.URL },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { PUBLIC_URL: string }

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

Effect-native Workers should prefer Cloudflare.Telemetry() over setting observability.traces by hand: providing the Layer enables traces on this Worker and mirrors Effect.withSpan into the Cloudflare waterfall. Pin compatibility: { date: "2026-08-25" } (or later) until the global default date is raised past 2026-07-28.

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,
},
},
}

A Tail Worker receives execution traces (console logs, exceptions, event metadata) from other Workers. List it in a producer’s tailConsumers and export a tail() handler from the consumer; Cloudflare delivers each invocation’s trace events to every listed consumer after the invocation completes.

Sending a Worker’s traces to a Tail Worker

const tailWorker = yield* Cloudflare.Worker("TailWorker", {
// exports: export default { async tail(events, env, ctx) { ... } }
main: "./src/tail.ts",
});
const api = yield* Cloudflare.Worker("Api", {
main: "./src/api.ts",
tailConsumers: [tailWorker],
});

A streaming Tail Worker receives the same invocation’s events live, while the producer is still executing: list it in streamingTailConsumers and export a tailStream() handler that is invoked with the invocation’s onset event and returns a handler for every subsequent event of the session, ending with the terminal outcome.

Streaming a Worker’s events to a streaming Tail Worker

const streamTailWorker = yield* Cloudflare.Worker("StreamTailWorker", {
// exports: export default {
// tailStream(onset, env, ctx) {
// return (event) => { ... }; // log, spanOpen, ..., outcome
// },
// }
main: "./src/stream-tail.ts",
});
const api = yield* Cloudflare.Worker("Api", {
main: "./src/api.ts",
streamingTailConsumers: [streamTailWorker],
});

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 Construction 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* () {
// Construction: 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,
},
}

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 constructor and used from any handler; its methods are RuntimeContext- colored, so they can only run inside a handler.

The constructor 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 constructor 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

// Construction
const 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 });
}),
};

Bind an R2 bucket in the Construction phase with Cloudflare.R2.ReadWriteBucket. The returned handle exposes get, put, delete, and list methods you can call in your runtime handlers.

// Construction
const 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 });
}),
};

Bind a KV namespace with Cloudflare.KV.ReadWriteNamespace. KV provides eventually-consistent, low-latency key-value reads replicated globally across Cloudflare’s edge.

// Construction
const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return {
fetch: Effect.gen(function* () {
const value = yield* kv.get("my-key");
return HttpServerResponse.text(value ?? "not found");
}),
};

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.

// Construction
const 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);
}),
};

Yield a DurableObject class in the Construction phase to get a namespace handle. Call getByName or getById to get a typed RPC stub, then call its methods from your runtime handlers.

// Construction
const 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 run long-lived processes alongside Durable Objects. Provide Cloudflare.Containers.layer(Sandbox, …) on a DO’s constructor 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 }),
),
),
) {}

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.

// Construction
const loader = yield* Cloudflare.WorkerLoader("Loader");
return {
fetch: Effect.gen(function* () {
const worker = yield* loader.load({
compatibilityDate: "2026-08-31",
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);
}),
};

Source: src/Cloudflare/Workers/WorkerEntrypoint.ts

Bind a specific WorkerEntrypoint class exported by another Worker.

Binding a Worker directly in env (env: { TARGET: worker }) targets its default entrypoint. A Worker that exposes additional WorkerEntrypoint classes — workerd treats every named class export of an entry module as an entrypoint — is bound with WorkerEntrypoint, which selects the class by name and can deliver ctx.props to it.

WorkerEntrypoint: Defining the Target Entrypoint

Section titled “WorkerEntrypoint: Defining the Target Entrypoint”

Export a class extending Cloudflare’s native WorkerEntrypoint from the target Worker’s module. This Api class defines the RPC methods that callers can invoke through a named service binding.

target/src/worker.ts
import { WorkerEntrypoint } from "cloudflare:workers";
export class Api extends WorkerEntrypoint {
async greet(name: string): Promise<string> {
return `hello ${name}`;
}
}
export default {
async fetch() {
return new Response("ok");
},
};

WorkerEntrypoint: Binding a Named Entrypoint

Section titled “WorkerEntrypoint: Binding a Named Entrypoint”

Import the exported Api class as a type and select its named export with "Api". Pass its instance type (Api, not typeof Api) to get Cloudflare’s native Service<Api> RPC client. Without a type argument, the binding is a bare Fetcher; the entrypoint name alone cannot identify the class’s type.

alchemy.run.ts
import * as Cloudflare from "alchemy/Cloudflare";
import type { Api } from "./target/src/worker.ts";
const target = yield* Cloudflare.Worker("Target", {
main: "./target/src/worker.ts",
});
const caller = yield* Cloudflare.Worker("Caller", {
main: "./caller/src/worker.ts",
env: {
API: Cloudflare.WorkerEntrypoint<Api>(target, "Api"),
},
});
caller/src/worker.ts
import type { CallerEnv } from "../../alchemy.run.ts";
export default {
async fetch(request: Request, env: CallerEnv) {
return new Response(await env.API.greet("alice"));
},
};

The options form attaches properties the target reads from this.ctx.props — workerd’s per-binding configuration channel. Output values resolve at deploy time.

env: {
VENDOR: Cloudflare.WorkerEntrypoint(vendorWorker, {
entrypoint: "Vendor",
props: { baseUrl: site.url },
}),
}

Source: src/Cloudflare/Workers/WorkerLoader.ts

Load and run ephemeral Workers at runtime from inline JavaScript modules.

WorkerLoader registers a worker_loader binding on the parent Worker at deploy time. At runtime you call .load() with inline module source code and get back a fully typed Worker instance you can fetch or call RPC methods on. Each loaded Worker runs in its own isolate with full sandboxing.

This is useful for evaluating user-provided code, running untrusted plugins, or dynamically generating Workers from templates.

Yield Cloudflare.WorkerLoader(name) in your Worker’s init phase to register the binding and get back a runtime handle. The string argument becomes the binding name on the deployed Worker.

Registering a loader (effect-native Worker)

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
export default class EvalWorker extends Cloudflare.Worker<EvalWorker>()(
"EvalWorker",
{ main: import.meta.url },
Effect.gen(function* () {
// Registers the `worker_loader` binding on this Worker
const loader = yield* Cloudflare.WorkerLoader("LOADER");
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const code = yield* request.text;
// Spin up an isolated, sandboxed Worker from inline source.
const worker = yield* loader.load({
compatibilityDate: "2026-08-31",
mainModule: "worker.js",
modules: {
"worker.js": `export default {
async fetch(req) {
const result = (0, eval)(await req.text());
return new Response(String(result));
}
}`,
},
globalOutbound: null, // block outbound network access
});
// Call the loaded Worker over Effect-native HTTP.
const response = yield* worker.fetch(
HttpClientRequest.post("https://worker/").pipe(
HttpClientRequest.bodyText(code),
),
);
return HttpServerResponse.fromClientResponse(response);
}),
};
}),
) {}

Declaring on env (async Worker)

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { LOADER: Cloudflare.WorkerLoader() },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// worker.ts
export default {
async fetch(req: Request, env: WorkerEnv) {
const worker = env.LOADER.load({
compatibilityDate: "2026-08-31",
mainModule: "worker.js",
modules: { "worker.js": "export default { fetch: () => new Response('ok') }" },
});
return worker.getEntrypoint().fetch(req);
},
};

Call loader.load() with a compatibility date, a main module name, and a map of module names to source code strings. The returned instance exposes .fetch() for HTTP and RPC methods for named entrypoints.

const worker = loader.load({
compatibilityDate: "2026-08-31",
mainModule: "worker.js",
modules: {
"worker.js": `export default {
async fetch(request) {
return new Response("Hello from dynamic worker!");
}
}`,
},
});
const response = yield* worker.fetch(
HttpClientRequest.get("https://worker/"),
);

Call loader.get(id, getCode) to address a dynamic Worker by name. If an isolate with that id is already warm it is reused; getCode runs only on a cold start. The returned stub is a WorkerStub: call .fetch() on it directly, or .getEntrypoint() for a named export.

const worker = yield* loader.get("eval", () => ({
compatibilityDate: "2026-08-31",
mainModule: "worker.js",
modules: {
"worker.js": `export default {
async fetch() {
return new Response("cached");
}
}`,
},
}));
const response = yield* worker.fetch(
HttpClientRequest.get("https://worker/"),
);

Set globalOutbound to null to block all outbound network access from the dynamic Worker, or pass an RPC stub to intercept and proxy outbound requests.

const worker = loader.load({
compatibilityDate: "2026-08-31",
mainModule: "worker.js",
modules: {
"worker.js": `export default {
async fetch(req) {
// fetch() calls from here will fail
return new Response("sandboxed");
}
}`,
},
globalOutbound: null,
});

If the dynamic Worker exports named entrypoints, use .getEntrypoint(name) to get a typed stub for calling its methods.

const worker = loader.load({ ... });
const api = worker.getEntrypoint<{ greet: (name: string) => Effect.Effect<string> }>("api");
const greeting = yield* api.greet("world");