1---
2title: Workers
3description: Cloudflare Workers are the compute Runtime of every Alchemy app — declare the Worker and its handler in one file, bind resources with full type safety, and call other Workers over schemaless RPC.
4---
5
6In Alchemy, a Cloudflare Worker is a
7[Runtime](/infrastructure-as-effects/runtime): a
8[Resource](/infrastructure-as-code/resource) that carries the code it
9runs. The name and props describe the Worker to deploy. The Effect is
10what it does:
11
12```typescript
13// src/worker.ts
14import * as Cloudflare from "alchemy/Cloudflare";
15import * as Effect from "effect/Effect";
16import * as HttpServerResponse from "effect/http/HttpServerResponse";
17
18export default Cloudflare.Worker(
19 "Worker",
20 { main: import.meta.url },
21 Effect.gen(function* () {
22 return {
23 fetch: Effect.gen(function* () {
24 return HttpServerResponse.text("Hello, world!");
25 }),
26 };
27 }),
28);
29```
30
31`main: import.meta.url` tells Alchemy to bundle this file's default
32export, the Worker itself, as the script. Yield the Worker from a
33Stack and expose its URL:
34
35```typescript
36// alchemy.run.ts
37import * as Alchemy from "alchemy";
38import * as Cloudflare from "alchemy/Cloudflare";
39import * as Effect from "effect/Effect";
40import Worker from "./src/worker.ts";
41
42export default Alchemy.Stack(
43 "MyApp",
44 { providers: Cloudflare.providers(), state: Cloudflare.state() },
45 Effect.gen(function* () {
46 const worker = yield* Worker;
47 return { url: worker.url };
48 }),
49);
50```
51
52```sh
53bun alchemy deploy
54```
55
56Alchemy bundles the file, uploads it, enables the `workers.dev`
57subdomain, and prints the URL as a stack output.
58
59Reach for a Worker whenever you need compute: HTTP APIs, frontends,
60queue consumers, cron jobs, RPC services. Every other building block,
61[Durable Objects](/cloudflare/compute/durable-objects),
62[D1](/cloudflare/data/d1), [R2](/cloudflare/data/r2),
63[Queues](/cloudflare/messaging/queues), is reached through a Worker
64by binding it.
65
66## Bind a resource
67
68Declare a Bucket next to the Worker and bind it. The
69[Binding](/infrastructure-as-effects/binding) hands back a typed
70client, and the Layer you provide decides how the binding is
71implemented, here as a native `r2_bucket` binding on the Worker:
72
73```typescript
74// src/worker.ts
75import * as Cloudflare from "alchemy/Cloudflare";
76import * as Effect from "effect/Effect";
77import * as HttpServerResponse from "effect/http/HttpServerResponse";
78
79export const Uploads = Cloudflare.R2.Bucket("Uploads");
80
81export default Cloudflare.Worker(
82 "Worker",
83 { main: import.meta.url },
84 Effect.gen(function* () {
85 const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads);
86
87 return {
88 fetch: Effect.gen(function* () {
89 const obj = yield* bucket.get("hello.txt");
90 return obj
91 ? HttpServerResponse.text(yield* obj.text())
92 : HttpServerResponse.text("Not found", { status: 404 });
93 }).pipe(
94 Effect.catchTag("R2Error", (error) =>
95 Effect.succeed(HttpServerResponse.text(error.message, { status: 500 })),
96 ),
97 ),
98 };
99 }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
100);
101```
102
103The binding is declared where it's used, so deploying the Worker
104deploys the wiring. The client's errors, like `R2Error`, live in the
105Effect type system, so you can't forget to handle them. And the access
106level is in the name: `ReadBucket`, `WriteBucket`, or
107`ReadWriteBucket`, depending on what the Worker actually needs.
108
109Every building block binds the same way. See
110[KV](/cloudflare/data/kv), [D1](/cloudflare/data/d1),
111[Queues](/cloudflare/messaging/queues),
112[Hyperdrive](/cloudflare/data/hyperdrive), and
113[Durable Objects](/cloudflare/compute/durable-objects), or walk
114through it step by step in [tutorial part 2](/cloudflare/tutorial/part-2).
115
116:::note
117[Bindings](/infrastructure-as-effects/binding) explains what a binding
118generates at deploy time and how the Layer you provide implements it.
119:::
120
121## Call another Worker
122
123Workers call each other's methods directly. No HTTP routes, no schema,
124no public URL. Declare the callee as a class whose type carries its
125RPC shape, and attach its implementation with `.make()`:
126
127```typescript
128// src/Greeter.ts
129import * as Cloudflare from "alchemy/Cloudflare";
130import * as Effect from "effect/Effect";
131
132export class Greeter extends Cloudflare.Worker<
133 Greeter,
134 { greet: (name: string) => Effect.Effect<string> }
135>()("Greeter") {}
136
137export default Greeter.make(
138 { main: import.meta.url },
139 Effect.gen(function* () {
140 return {
141 greet: (name: string) => Effect.succeed(`Hello ${name}`),
142 };
143 }),
144);
145```
146
147Bind it from another Worker with `bindWorker` and call `greet` through
148the typed stub:
149
150```typescript
151// src/Api.ts
152import * as Cloudflare from "alchemy/Cloudflare";
153import * as Effect from "effect/Effect";
154import * as HttpServerResponse from "effect/http/HttpServerResponse";
155import { Greeter } from "./Greeter.ts";
156
157export default Cloudflare.Worker(
158 "Api",
159 { main: import.meta.url },
160 Effect.gen(function* () {
161 const greeter = yield* Cloudflare.Workers.bindWorker(Greeter);
162
163 return {
164 fetch: Effect.gen(function* () {
165 return HttpServerResponse.text(yield* greeter.greet("world"));
166 }),
167 };
168 }),
169);
170```
171
172`bindWorker(Greeter)` registers a service binding on `Api`, so each
173call travels over Cloudflare's in-account service-binding fabric and
174never the public internet. `greeter.greet("world")` is type-checked
175against `Greeter`'s declared shape end to end.
176
177Importing the `Greeter` class pulls in no runtime code. Alchemy marks
178`.make()` as pure, so the bundler drops the implementation from every
179Worker that only binds it. For external clients across a trust
180boundary, where payloads need schema validation before they touch your
181code, reach for [Effect RPC](/cloudflare/apis/effect-rpc) instead.
182
183:::note
184[Runtime](/infrastructure-as-effects/runtime#three-ways-to-declare-a-runtime)
185covers the three declaration forms, and
186[Schemaless RPC](/apis/schemaless) the calling convention.
187:::
188
189## Serve static assets
190
191Pass `assets` and Cloudflare serves matching requests from its asset
192layer. Everything else invokes your handlers:
193
194```typescript
195export default Cloudflare.Worker(
196 "Worker",
197 { main: import.meta.url, assets: "./public" },
198 // ...
199);
200```
201
202A path that matches no asset already falls through to the Worker. Set
203`runWorkerFirst` when a path the Worker must own could be answered by
204the asset layer instead, such as an asset file shadowing a route or a
205single-page-application fallback that would serve the app shell to an
206API call:
207
208```typescript
209assets: {
210 directory: "./public",
211 // only these paths reach the Worker ahead of assets
212 runWorkerFirst: ["/api/*", "/admin/*"],
213},
214```
215
216`runWorkerFirst: true` routes every request through the Worker, which
217then serves files itself via the `ASSETS` binding. The same routing
218applies under `alchemy dev`.
219
220Omit `main` entirely for an assets-only Worker. No script is uploaded,
221and the asset layer applies `htmlHandling` and `notFoundHandling`
222itself. Static asset requests are free and never invoke a Worker:
223
224```typescript
225const site = yield* Cloudflare.Worker("Site", {
226 assets: {
227 directory: "./public",
228 htmlHandling: "drop-trailing-slash",
229 notFoundHandling: "404-page",
230 },
231 domain: "static.example.com",
232});
233```
234
235A `_headers` or `_redirects` file in the directory is applied
236automatically, and a `.assetsignore` file excludes files from the
237upload. If the directory comes from a build command, use
238[StaticSite](/cloudflare/frontend/static-site). For Vite projects, use
239the [Vite resource](/cloudflare/frontend/vite).
240
241## URLs and domains
242
243`worker.urls` is every URL that serves the Worker, most significant
244first, and `worker.url` is always `urls[0]`. Attach custom domains
245with `domain`:
246
247```typescript
248const worker = yield* Cloudflare.Worker("Api", {
249 main: "./src/api.ts",
250 domain: {
251 name: "example.com",
252 aliases: ["www.example.com"],
253 redirects: ["old.example.com"], // 301 → https://example.com
254 },
255});
256// worker.url === "https://example.com"
257// worker.urls === ["https://example.com", "https://www.example.com",
258// "https://<name>.<account>.workers.dev"]
259```
260
261Every hostname becomes a Cloudflare custom domain. DNS records and
262edge certificates are managed for you, and the zone must already exist
263in the account. A bare string is shorthand for `{ name }`. Redirect
264hostnames answer with a permanent redirect before the Worker runs, so
265they never appear in `urls`.
266
267Zone routes attach the Worker to a path pattern instead of a whole
268hostname:
269
270```typescript
271routes: [
272 { pattern: "example.com/api/*", zoneName: "example.com" },
273],
274```
275
276`workersDev` controls the `workers.dev` surface:
277
278```typescript
279workersDev: false, // no workers.dev URLs, url is the custom domain
280workersDev: { enabled: false, previewsEnabled: true }, // preview URLs only
281```
282
283Under `alchemy dev`, `urls` is the dev server's actual surface, the
284`localhost` address first and then the LAN addresses. Pass `urls`
285wholesale wherever a list of origins is needed:
286
287```typescript
288const api = yield* Cloudflare.Worker("Api", {
289 main: "./src/api.ts",
290 env: { ALLOWED_ORIGINS: site.urls }, // CORS allow-list
291});
292```
293
294For zone setup, DNS records, and route patterns, see
295[custom domains](/cloudflare/networking/custom-domains).
296
297## The Worker's own URL
298
299A Worker often needs the URL it is served at, to build absolute links
300or register a webhook. That URL only exists at deploy time, and a
301Worker can't reference its own `url` Output, so `Worker.URL` injects
302it as a binding on the Worker itself. Yield it in the Construction
303phase, then yield the accessor it returns inside a handler:
304
305```typescript
306export default Cloudflare.Worker(
307 "Api",
308 { main: import.meta.url },
309 Effect.gen(function* () {
310 const url = yield* Cloudflare.Worker.URL;
311
312 return {
313 fetch: Effect.gen(function* () {
314 const publicUrl = yield* url;
315 return yield* HttpServerResponse.json({ url: publicUrl });
316 }),
317 };
318 }),
319);
320```
321
322The value is the first custom `domain` if one is configured, otherwise
323the `workers.dev` URL, and always equals the resource's `url`
324attribute. Under `alchemy dev` it is the local dev server's URL.
325
326Async Workers declare it on `env`, where `InferEnv` types the entry as
327`string`. A `VITE_`-prefixed entry is also inlined into a Vite site's
328client bundle, see [the site's own URL](/cloudflare/frontend/vite#the-sites-own-url):
329
330```typescript
331export const Worker = Cloudflare.Worker("Worker", {
332 main: "./src/worker.ts",
333 env: { PUBLIC_URL: Cloudflare.Worker.URL },
334});
335```
336
337## Async Workers
338
339A Worker doesn't have to be an Effect program. Point `main` at a plain
340module, a classic `async fetch` handler or a prebuilt bundle from
341another tool, and declare bindings with the `env` prop. `InferEnv`
342derives the handler's env type from them:
343
344```typescript
345// alchemy.run.ts
346import * as Cloudflare from "alchemy/Cloudflare";
347
348export const Uploads = Cloudflare.R2.Bucket("Uploads");
349export const DB = Cloudflare.D1.Database("DB");
350
351export const Worker = Cloudflare.Worker("Worker", {
352 main: "./src/worker.ts",
353 env: { Uploads, DB },
354});
355
356export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
357```
358
359```typescript
360// src/worker.ts
361import type { WorkerEnv } from "../alchemy.run.ts";
362
363export default {
364 async fetch(request: Request, env: WorkerEnv) {
365 const obj = await env.Uploads.get("hello.txt");
366 return obj
367 ? new Response(await obj.text())
368 : new Response("Not found", { status: 404 });
369 },
370};
371```
372
373`InferEnv` maps each entry to its native `workers-types` client. An R2
374bucket becomes `R2Bucket`, a D1 database `D1Database`, a Durable
375Object or [Container](/cloudflare/compute/containers#bind-on-an-async-worker)
376a typed `DurableObjectNamespace`, and `Config` or `Redacted` values
377`string`. The handler stays plain JavaScript, but the env can never
378drift from the infrastructure that produced it.
379[Python Workers](/cloudflare/compute/python-workers) are async Workers
380whose `main` is a `.py` file.
381
382### Named entrypoints
383
384Binding another Worker on `env` targets its default entrypoint. To expose a
385named RPC entrypoint, export a class extending Cloudflare's native
386[`WorkerEntrypoint`](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/)
387from the target Worker's module:
388
389```typescript
390// src/target.ts
391import { WorkerEntrypoint } from "cloudflare:workers";
392
393export class Api extends WorkerEntrypoint {
394 async greet(name: string): Promise<string> {
395 return `hello ${name}`;
396 }
397}
398
399export default {
400 async fetch() {
401 return new Response("ok");
402 },
403};
404```
405
406Import that `Api` class as a type in your infrastructure code.
407`Cloudflare.WorkerEntrypoint<Api>(target, "Api")` binds the named `Api`
408export on the target Worker. The type argument is the class's instance
409type (`Api`, not `typeof Api`):
410
411```typescript
412// alchemy.run.ts
413import * as Cloudflare from "alchemy/Cloudflare";
414import type { Api } from "./src/target.ts";
415
416const target = yield* Cloudflare.Worker("Target", {
417 main: "./src/target.ts",
418});
419
420const caller = yield* Cloudflare.Worker("Caller", {
421 main: "./src/caller.ts",
422 env: {
423 API: Cloudflare.WorkerEntrypoint<Api>(target, "Api"),
424 },
425});
426```
427
428`InferEnv` maps the binding to Cloudflare's native `Service<Api>` type,
429which checks method arguments and converts return values to promises:
430
431```typescript
432import type { CallerEnv } from "../alchemy.run.ts";
433
434export default {
435 async fetch(request: Request, env: CallerEnv) {
436 return new Response(await env.API.greet("alice"));
437 },
438};
439```
440
441Without the type argument the entry is a bare `Fetcher` service stub
442(`fetch` + `connect` only) and RPC calls do not type-check.
443
444The options form attaches properties the target reads from
445`this.ctx.props`, workerd's per-binding configuration channel. `Output`
446values resolve at deploy time:
447
448```typescript
449env: {
450 VENDOR: Cloudflare.WorkerEntrypoint(vendorWorker, {
451 entrypoint: "Vendor",
452 props: { baseUrl: site.url },
453 }),
454},
455```
456
457:::caution
458`alchemy dev` delivers `props` to the local workerd today. On deployed
459Workers the Cloudflare API's binding schema does not carry the field
460yet, so `props` are dropped at upload. The binding itself deploys
461correctly either way.
462:::
463
464## Configure the Worker
465
466Compatibility flags and the compatibility date go on `compatibility`.
467`nodejs_compat` is the one you reach for most:
468
469```typescript
470{
471 main: import.meta.url,
472 compatibility: { flags: ["nodejs_compat"], date: "2026-08-31" },
473}
474```
475
476Workers Observability is on by default, with logs and invocation logs
477enabled. Pass `observability` to tune sampling, persist logs, or turn
478on traces. Effect-native Workers should provide
479[`Cloudflare.Telemetry()`](/infrastructure-as-effects/telemetry)
480instead of setting `traces` by hand, since the Layer enables traces on
481the Worker and mirrors `Effect.withSpan` into the Cloudflare waterfall:
482
483```typescript
484observability: {
485 logs: { enabled: true, invocationLogs: true, persist: true },
486 traces: { enabled: true, headSamplingRate: 1 },
487},
488```
489
490A [Tail Worker](https://developers.cloudflare.com/workers/observability/logs/tail-workers/)
491receives another Worker's execution traces after each invocation. List
492it in `tailConsumers`, or in `streamingTailConsumers` to receive
493events live while the producer is still running:
494
495```typescript
496const tail = yield* Cloudflare.Worker("Tail", { main: "./src/tail.ts" });
497
498const api = yield* Cloudflare.Worker("Api", {
499 main: "./src/api.ts",
500 tailConsumers: [tail],
501});
502```
503
504Put Cloudflare Access in front of the Worker with `access`.
505Unauthenticated requests are redirected to your team's login page, and
506handlers read the identity from `Cloudflare.Access.Context`:
507
508```typescript
509{
510 main: import.meta.url,
511 access: {
512 policies: [{ decision: "allow", include: [{ emailDomain: "example.com" }] }],
513 },
514}
515```
516
517Pass an existing `Cloudflare.Access.Application` instead to share one
518policy set across several Workers. See
519[Protect a Worker with Access](/cloudflare/security/access).
520
521`main` is bundled with rolldown at deploy time. Effect, Alchemy, and
522the cloud SDKs are marked pure so unused code prunes aggressively.
523Mark your own side-effect-free packages the same way with
524`build.pure`, or set `bundle: false` to upload a bundle another tool
525already produced, such as OpenNext, byte for byte:
526
527```typescript
528{
529 main: "./.open-next/worker.js",
530 bundle: false,
531 assets: "./.open-next/assets",
532}
533```
534
535Every prop, including `limits`, `placement`, `logpush`, `crons`, and
536`tags`, is documented on the
537[Worker reference](/providers/cloudflare/workers/worker).
538
539## Background work and scopes
540
541The Worker's constructor runs once per isolate. The first event builds
542your Layers, and every later event reuses them. Each event then runs
543with a fresh `Scope` that closes after the response via
544`ctx.waitUntil`, so a finalizer added in a handler runs after the
545response is sent without blocking it:
546
547```typescript
548fetch: Effect.gen(function* () {
549 yield* Effect.addFinalizer(() => flushMetrics().pipe(Effect.ignore));
550 return HttpServerResponse.text("ok");
551}),
552```
553
554For ad-hoc background work, `WorkerExecutionContext.waitUntil` forks
555an Effect with the caller's full context and keeps the invocation
556alive until it settles:
557
558```typescript
559Effect.gen(function* () {
560 const exec = yield* Cloudflare.WorkerExecutionContext;
561
562 return {
563 fetch: Effect.gen(function* () {
564 yield* exec.waitUntil(writeAuditLog(event));
565 return HttpServerResponse.text("accepted", { status: 202 });
566 }),
567 };
568});
569```
570
571Streaming responses and WebSocket upgrades transfer the request scope
572to the stream, so its finalizers run when the stream completes. workerd
573has no isolate-teardown hook, so a finalizer added in the constructor
574never runs. Acquire anything that needs cleanup, connections and
575pools, inside handlers. `Drizzle.Postgres` follows this pattern, one
576pool per event, and the
577[SQL connection lifecycle](/sql/effect-sql/lifecycle) spells out the
578contract. [Instance scope vs request scope](/infrastructure-as-effects/runtime#instance-scope-vs-request-scope)
579covers the model across all runtimes.
580
581## Versions
582
583Every deploy uploads an immutable version, and by default it takes
584100% of traffic. The `version` prop unlocks the other shapes. Split
585traffic to roll a deploy out gradually:
586
587```typescript
588yield* Cloudflare.Worker("Api", {
589 main: "./src/api.ts",
590 version: { traffic: 25 },
591});
592```
593
594Set `preview.of` to deploy this Worker as a
595[Preview](/cloudflare/compute/previews) of another stage's Worker
596(branch and pull-request testing, isolated Durable Objects). Set
597`version.parent` to upload a canary version of another stage's
598Worker. Ramping, rollback, smoke testing with version overrides, and
599pinning users to a version are covered in
600[Gradual deployments](/cloudflare/compute/gradual-deployments).
601
602The `version_metadata` binding tells a running Worker which version
603it is, so responses and logs can say exactly which deploy produced
604them:
605
606```typescript
607export default Cloudflare.Worker(
608 "Api",
609 { main: import.meta.url },
610 Effect.gen(function* () {
611 const versionMetadata = yield* Cloudflare.Workers.VersionMetadata();
612
613 return {
614 fetch: Effect.gen(function* () {
615 const { id, tag, timestamp } = yield* versionMetadata;
616 return yield* HttpServerResponse.json({ id, tag, timestamp });
617 }),
618 };
619 }).pipe(Effect.provide(Cloudflare.Workers.VersionMetadataBinding)),
620);
621```
622
623Async Workers declare it on `env` as
624`CF_VERSION_METADATA: Cloudflare.Workers.VersionMetadata()`, and
625`InferEnv` types the entry as the native `{ id, tag, timestamp }`
626object.
627
628## Where next
629
630Guides that build on Workers:
631
632- [Gradual deployments](/cloudflare/compute/gradual-deployments) — previews, canaries, and ramped rollouts across versions.
633- [Python Workers](/cloudflare/compute/python-workers) — point `main` at a `.py` file.
634- [Workers Cache](/cloudflare/compute/cache) — serve responses from the edge before the Worker runs.
635- [Rate limiting](/cloudflare/compute/rate-limiting) — throttle requests with a binding.
636- [Browser rendering](/cloudflare/compute/browser-rendering) — a headless browser as a binding.
637- [Worker Loader](/cloudflare/compute/worker-loader) — run untrusted Workers at runtime.
638- [Workers for Platforms](/cloudflare/compute/workers-for-platforms) — run customers' Workers in your account.
639- [Custom domains](/cloudflare/networking/custom-domains) — serve Workers from your own hostnames and routes.
640- [Effect HTTP API](/cloudflare/apis/effect-http-api) and [Effect RPC](/cloudflare/apis/effect-rpc) — schema-validated surfaces.
641- [Frontend frameworks](/cloudflare/frontend/frontends) — ship a frontend from the same Stack.
642
643Related:
644
645- [Durable Objects](/cloudflare/compute/durable-objects), [Workflows](/cloudflare/compute/workflows), and [Containers](/cloudflare/compute/containers) — the other compute Runtimes, all reached through a Worker.
646- [Secrets & env](/cloudflare/security/secrets-env) — bind `.env` values and secrets into the Worker.
647
648Reference:
649
650- [Worker API reference](/providers/cloudflare/workers/worker)