Workers
A Worker is a program Cloudflare runs on every machine in its network. A request goes to the city nearest the person who sent it, and a Worker there handles it. There’s no region to choose, no server to size, and nothing running while nobody calls it.
How a Worker runs
Section titled “How a Worker runs”Workers run in isolates: lightweight sandboxes inside one V8 process, the JavaScript engine Chrome uses. Starting an isolate takes milliseconds, so a Worker can start for the first request in a city and still answer quickly. Many Workers, from many customers, share one process, each isolated from the others.
So a Worker keeps nothing between requests that it can count on. Two requests may run in two cities, in two isolates. Anything that must be remembered lives in a resource the Worker reaches through a binding: a KV Namespace, an R2 Bucket, a D1 database, or a Durable Object when one place has to see every request.
Each request gets a budget of CPU time, not wall-clock time. Time spent waiting on a fetch or a binding doesn’t count, so a Worker can wait on slow APIs without running out.
Declare a Worker
Section titled “Declare a Worker”In Alchemy, a Worker is a Runtime: a Resource that carries the code it runs. The name and props describe the Worker to deploy. The Effect is what it does:
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { return HttpServerResponse.text("Hello, world!"); }), }; }),);main: import.meta.url tells Alchemy to bundle this file’s default
export, the Worker itself, as the script. Yield the Worker from a
Stack and expose its URL:
import * as Alchemy from "alchemy";import * as Cloudflare from "@alchemy.run/cloudflare";import * as Effect from "effect/Effect";import Worker from "./src/worker.ts";
export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { const worker = yield* Worker; return { url: worker.url }; }),);pnpm alchemy deploybun alchemy deploynpx alchemy deployyarn alchemy deployAlchemy bundles the file, uploads it, enables the workers.dev
subdomain, and prints the URL as a stack output.
Reach for a Worker whenever you need compute: HTTP APIs, frontends, queue consumers, cron jobs, RPC services. Every other building block, Durable Objects, D1, R2, Queues, is reached through a Worker by binding it.
Bind a resource
Section titled “Bind a resource”Declare a Bucket next to the Worker and bind it. The
Binding hands back a typed
client, and the Layer you provide decides how the binding is
implemented, here as a native r2_bucket binding on the Worker:
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads);
return { fetch: Effect.gen(function* () { const obj = yield* bucket.get("hello.txt"); return obj ? HttpServerResponse.text(yield* obj.text()) : HttpServerResponse.text("Not found", { status: 404 }); }).pipe( Effect.catchTag("R2Error", (error) => Effect.succeed(HttpServerResponse.text(error.message, { status: 500 })), ), ), }; }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),);The binding is declared where it’s used, so deploying the Worker
deploys the wiring. The client’s errors, like R2Error, live in the
Effect type system, so you can’t forget to handle them. And the access
level is in the name: ReadBucket, WriteBucket, or
ReadWriteBucket, depending on what the Worker actually needs.
Every building block binds the same way. See KV, D1, Queues, Hyperdrive, and Durable Objects, or walk through it step by step in tutorial part 2.
Call another Worker
Section titled “Call another Worker”Workers call each other’s methods directly. No HTTP routes, no schema,
no public URL. Declare the callee as a class whose type carries its
RPC shape, and attach its implementation with .make():
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Effect from "effect/Effect";
export class Greeter extends Cloudflare.Worker< Greeter, { greet: (name: string) => Effect.Effect<string> }>()("Greeter") {}
export default Greeter.make( { main: import.meta.url }, Effect.gen(function* () { return { greet: (name: string) => Effect.succeed(`Hello ${name}`), }; }),);Bind it from another Worker with bindWorker and call greet through
the typed stub:
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { Greeter } from "./Greeter.ts";
export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const greeter = yield* Cloudflare.Workers.bindWorker(Greeter);
return { fetch: Effect.gen(function* () { return HttpServerResponse.text(yield* greeter.greet("world")); }), }; }),);bindWorker(Greeter) registers a service binding on Api, so each
call travels over Cloudflare’s in-account service-binding fabric and
never the public internet. greeter.greet("world") is type-checked
against Greeter’s declared shape end to end.
Importing the Greeter class pulls in no runtime code. Alchemy marks
.make() as pure, so the bundler drops the implementation from every
Worker that only binds it. For external clients across a trust
boundary, where payloads need schema validation before they touch your
code, reach for Effect RPC instead.
Call a Worker in another stack
Section titled “Call a Worker in another stack”When Greeter is deployed by another stack, reference it with
Worker.ref. Pass the Greeter class rather than its logical id so the
reference keeps its RPC shape:
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { Greeter } from "./Greeter.ts";
export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const greeter = yield* Cloudflare.Workers.bindWorker( Cloudflare.Worker.ref(Greeter, { stack: "Greeter" }), );
return { fetch: Effect.gen(function* () { return HttpServerResponse.text(yield* greeter.greet("world")); }), }; }),);Worker.ref(Greeter, ...) only reads Greeter’s logical id. This stack
does not deploy Greeter; it binds to the one the Greeter stack owns.
Call the Worker’s own RPC methods
Section titled “Call the Worker’s own RPC methods”A Worker can bind itself. Declare the class first so its implementation can reference it:
export class Greeter extends Cloudflare.Worker< Greeter, { greet: (name: string) => Effect.Effect<string> }>()("Greeter") {}
export default Greeter.make( { main: import.meta.url }, Effect.gen(function* () { const self = yield* Cloudflare.Workers.bindWorker(Greeter);
return { greet: (name: string) => Effect.succeed(`Hello ${name}`), fetch: Effect.gen(function* () { return HttpServerResponse.text(yield* self.greet("world")); }), }; }),);A self binding uses workerd’s ctx.exports loopback, so nothing is added
to the Worker’s deployed bindings.
Serve static assets
Section titled “Serve static assets”Pass assets and Cloudflare serves matching requests from its asset
layer. Everything else invokes your handlers:
export default Cloudflare.Worker( "Worker", { main: import.meta.url, assets: "./public" }, // ...);A path that matches no asset already falls through to the Worker. Set
runWorkerFirst when a path the Worker must own could be answered by
the asset layer instead, such as an asset file shadowing a route or a
single-page-application fallback that would serve the app shell to an
API call:
assets: { directory: "./public", // only these paths reach the Worker ahead of assets runWorkerFirst: ["/api/*", "/admin/*"],},runWorkerFirst: true routes every request through the Worker, which
then serves files itself via the ASSETS binding. The same routing
applies under alchemy dev.
Omit main entirely for an assets-only Worker. No script is uploaded,
and the asset layer applies htmlHandling and notFoundHandling
itself. Static asset requests are free and never invoke a Worker:
const site = yield* Cloudflare.Worker("Site", { assets: { directory: "./public", htmlHandling: "drop-trailing-slash", notFoundHandling: "404-page", }, domain: "static.example.com",});A _headers or _redirects file in the directory is applied
automatically, and a .assetsignore file excludes files from the
upload. If the directory comes from a build command, use
StaticSite. For Vite projects, use
the Vite resource.
URLs and domains
Section titled “URLs and domains”worker.urls is every URL that serves the Worker, most significant
first, and worker.url is always urls[0]. Attach custom domains
with domain:
const worker = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", domain: { name: "example.com", 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"]Every hostname becomes a Cloudflare custom domain. DNS records and
edge certificates are managed for you, and the zone must already exist
in the account. A bare string is shorthand for { name }. Redirect
hostnames answer with a permanent redirect before the Worker runs, so
they never appear in urls.
Zone routes attach the Worker to a path pattern instead of a whole hostname:
routes: [ { pattern: "example.com/api/*", zoneName: "example.com" },],workersDev controls the workers.dev surface:
workersDev: false, // no workers.dev URLs, url is the custom domainworkersDev: { enabled: false, previewsEnabled: true }, // preview URLs onlyUnder alchemy dev, urls is the dev server’s actual surface, the
localhost address first and then the LAN addresses. Pass urls
wholesale wherever a list of origins is needed:
const api = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", env: { ALLOWED_ORIGINS: site.urls }, // CORS allow-list});For zone setup, DNS records, and route patterns, see custom domains.
The Worker’s own URL
Section titled “The Worker’s own URL”A Worker often needs the URL it is served at, to build absolute links
or register a webhook. That URL only exists at deploy time, and a
Worker can’t reference its own url Output, so Worker.URL injects
it as a binding on the Worker itself. Yield it in the Construction
phase, then yield the accessor it returns inside a handler:
export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const url = yield* Cloudflare.Worker.URL;
return { fetch: Effect.gen(function* () { const publicUrl = yield* url; return yield* HttpServerResponse.json({ url: publicUrl }); }), }; }),);The value is the first custom domain if one is configured, otherwise
the workers.dev URL, and always equals the resource’s url
attribute. Under alchemy dev it is the local dev server’s URL.
Async Workers declare it on env, where InferEnv types the entry as
string. A VITE_-prefixed entry is also inlined into a Vite site’s
client bundle, see the site’s own URL:
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { PUBLIC_URL: Cloudflare.Worker.URL },});Async Workers
Section titled “Async Workers”A Worker doesn’t have to be an Effect program. Point main at a plain
module, a classic async fetch handler or a prebuilt bundle from
another tool, and declare bindings with the env prop. InferEnv
derives the handler’s env type from them:
import * as Cloudflare from "@alchemy.run/cloudflare";
export const Uploads = Cloudflare.R2.Bucket("Uploads");export const DB = Cloudflare.D1.Database("DB");
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { Uploads, DB },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;import type { WorkerEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WorkerEnv) { const obj = await env.Uploads.get("hello.txt"); return obj ? new Response(await obj.text()) : new Response("Not found", { status: 404 }); },};InferEnv maps each entry to its native workers-types client. An R2
bucket becomes R2Bucket, a D1 database D1Database, a Durable
Object or Container
a typed DurableObjectNamespace, and Config or Redacted values
string. The handler stays plain JavaScript, but the env can never
drift from the infrastructure that produced it.
Python Workers are async Workers
whose main is a .py file.
Bind another Worker or this one
Section titled “Bind another Worker or this one”A service binding reaches a Worker’s default export or one of its named
WorkerEntrypoint
classes. The target can be another Worker or this Worker itself. The
examples below call this module:
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"); },};Another Worker’s default export
Section titled “Another Worker’s default export”Put the Worker itself on env:
const target = yield* Cloudflare.Worker("Target", { main: "./src/target.ts" });
const caller = yield* Cloudflare.Worker("Caller", { main: "./src/caller.ts", env: { TARGET: target },});env.TARGET is a Fetcher, so env.TARGET.fetch(request) reaches the
target’s export default.
Another Worker’s named entrypoint
Section titled “Another Worker’s named entrypoint”Name the class with Cloudflare.WorkerEntrypoint, and pass its instance
type (Api, not typeof Api) to type the RPC methods:
import type { Api } from "./src/target.ts";
const caller = yield* Cloudflare.Worker("Caller", { main: "./src/caller.ts", env: { API: Cloudflare.WorkerEntrypoint<Api>(target, "Api") },});export default { async fetch(request: Request, env: CallerEnv) { return new Response(await env.API.greet("alice")); },};InferEnv maps the binding to Cloudflare’s Service<Api>, which checks
arguments and returns promises. Without the type argument env.API is a
plain Fetcher with only fetch and connect.
This Worker’s default export
Section titled “This Worker’s default export”Cloudflare.WorkerEntrypoint() with no arguments binds the Worker to itself:
const api = yield* Cloudflare.Worker("Api", { main: "./src/target.ts", env: { SELF: Cloudflare.WorkerEntrypoint() },});env.SELF.fetch(request) re-enters the Worker’s own export default.
Cloudflare.Workers.Self is the same binding.
This Worker’s named entrypoint
Section titled “This Worker’s named entrypoint”Pass just the name:
import type { Api } from "./src/target.ts";
const api = yield* Cloudflare.Worker("Api", { main: "./src/target.ts", env: { API: Cloudflare.WorkerEntrypoint<Api>("Api") },});When the entrypoint class extends WorkerEntrypoint<WorkerEnv>, declare
WorkerEnv as an interface. A type alias is a circular type here:
export interface WorkerEnv extends Cloudflare.InferEnv<typeof api> {}Without the type argument env.API is a plain Fetcher, and either form
of WorkerEnv works.
Pass ctx.props to an entrypoint
Section titled “Pass ctx.props to an entrypoint”The options form attaches properties the entrypoint reads from
this.ctx.props, on another Worker or on this one
(Cloudflare.WorkerEntrypoint({ entrypoint, props })). Output values
resolve at deploy time:
env: { VENDOR: Cloudflare.WorkerEntrypoint(vendorWorker, { entrypoint: "Vendor", props: { baseUrl: site.url }, }),},Leave out entrypoint to target the default export.
Configure the Worker
Section titled “Configure the Worker”Compatibility flags and the compatibility date go on compatibility.
nodejs_compat is the one you reach for most:
{ main: import.meta.url, compatibility: { flags: ["nodejs_compat"], date: "2026-08-31" },}Workers Observability is on by default, with logs and invocation logs
enabled. Pass observability to tune sampling, persist logs, or turn
on traces. Effect-native Workers should provide
Cloudflare.Telemetry()
instead of setting traces by hand, since the Layer enables traces on
the Worker and mirrors Effect.withSpan into the Cloudflare waterfall:
observability: { logs: { enabled: true, invocationLogs: true, persist: true }, traces: { enabled: true, headSamplingRate: 1 },},A Tail Worker
receives another Worker’s execution traces after each invocation. List
it in tailConsumers, or in streamingTailConsumers to receive
events live while the producer is still running:
const tail = yield* Cloudflare.Worker("Tail", { main: "./src/tail.ts" });
const api = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", tailConsumers: [tail],});Put Cloudflare Access in front of the Worker with access.
Unauthenticated requests are redirected to your team’s login page, and
handlers read the identity from Cloudflare.Access.Context:
{ main: import.meta.url, access: { policies: [{ decision: "allow", include: [{ emailDomain: "example.com" }] }], },}Pass an existing Cloudflare.Access.Application instead to share one
policy set across several Workers. See
Protect a Worker with Access.
main is bundled with rolldown at deploy time. Effect, Alchemy, and
the cloud SDKs are marked pure so unused code prunes aggressively.
Mark your own side-effect-free packages the same way with
build.pure, or set bundle: false to upload a bundle another tool
already produced, such as OpenNext, byte for byte:
{ main: "./.open-next/worker.js", bundle: false, assets: "./.open-next/assets",}Every prop, including limits, placement, logpush, crons, and
tags, is documented on the
Worker reference.
Background work and scopes
Section titled “Background work and scopes”The Worker’s constructor runs once per isolate. The first event builds
your Layers, and every later event reuses them. Each event then runs
with a fresh Scope that closes after the response via
ctx.waitUntil, so a finalizer added in a handler runs after the
response is sent without blocking it:
fetch: Effect.gen(function* () { yield* Effect.addFinalizer(() => flushMetrics().pipe(Effect.ignore)); return HttpServerResponse.text("ok");}),For ad-hoc background work, Cloudflare.waitUntil forks an Effect
with the caller’s full context and keeps the invocation alive until it
settles:
Effect.gen(function* () { return { fetch: Effect.gen(function* () { yield* Cloudflare.waitUntil(writeAuditLog(event)); return HttpServerResponse.text("accepted", { status: 202 }); }), };});Streaming responses and WebSocket upgrades transfer the request scope
to the stream, so its finalizers run when the stream completes. workerd
has no isolate-teardown hook, so a finalizer added in the constructor
never runs. Acquire anything that needs cleanup, connections and
pools, inside handlers. Drizzle.Postgres follows this pattern, one
pool per event, and the
SQL connection lifecycle spells out the
contract. Instance scope vs request scope
covers the model across all runtimes.
Versions
Section titled “Versions”Every deploy uploads an immutable version, and by default it takes
100% of traffic. The version prop unlocks the other shapes. Split
traffic to roll a deploy out gradually:
yield* Cloudflare.Worker("Api", { main: "./src/api.ts", version: { traffic: 25 },});Set preview.of to deploy this Worker as a
Preview of another stage’s Worker
(branch and pull-request testing, isolated Durable Objects). Set
version.parent to upload a canary version of another stage’s
Worker. Ramping, rollback, smoke testing with version overrides, and
pinning users to a version are covered in
Gradual deployments.
Durable Objects that are busy during a deploy keep running their
current code for up to 5 minutes. codeUpdateStrategy changes how
long they wait, or switches them right away. See
Code updates.
The version_metadata binding tells a running Worker which version
it is, so responses and logs can say exactly which deploy produced
them:
export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const versionMetadata = yield* Cloudflare.Workers.VersionMetadata();
return { fetch: Effect.gen(function* () { const { id, tag, timestamp } = yield* versionMetadata; return yield* HttpServerResponse.json({ id, tag, timestamp }); }), }; }).pipe(Effect.provide(Cloudflare.Workers.VersionMetadataBinding)),);Async Workers declare it on env as
CF_VERSION_METADATA: Cloudflare.Workers.VersionMetadata(), and
InferEnv types the entry as the native { id, tag, timestamp }
object.
Where next
Section titled “Where next”Workers have more to them than one chapter covers. Code updates explains what a deploy does to running Workers, and Gradual deployments rolls versions out a share at a time. Python Workers, Workers Cache, Rate limiting, Browser rendering, Worker Loader and Workers for Platforms each add one capability.
Every Worker prop is in the Worker reference.