Skip to content

Workers

A Cloudflare Worker is serverless JavaScript running in every Cloudflare data center. In alchemy, a Worker is a special kind of Resource: it has both an infrastructure definition (name, bundle, compatibility flags) and a runtime implementation expressed as an Effect — one program describes what to deploy and what it does.

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 via bindings.

A Worker follows a two-phase pattern: the outer Effect.gen is the init phase (binds resources at deploy time), and the handlers it returns are the runtime phase (run on each request).

src/worker.ts
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* () {
return {
fetch: Effect.gen(function* () {
return HttpServerResponse.text("Hello, world!");
}),
};
}),
);

Wire it into a Stack and expose its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/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 };
}),
);
Terminal window
bun alchemy deploy

Alchemy bundles the file, uploads it, enables the workers.dev subdomain, and prints the URL as a stack output.

Workers reach other resources through bindings — typed clients resolved in the init phase. Given an R2 bucket declared in its own file (export const Bucket = Cloudflare.R2.Bucket("Bucket")), bind it by yielding Cloudflare.R2.ReadWriteBucket(...) and providing its binding layer:

src/worker.ts
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import { Bucket } from "./bucket.ts";
export default Cloudflare.Worker(
"Worker",
{ main: import.meta.url },
Effect.gen(function* () {
// init: register the binding, get a typed client
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
return {
fetch: Effect.gen(function* () {
// runtime: use it
const object = yield* bucket.get("hello.txt");
return HttpServerResponse.text(
object === null ? "Not found" : yield* object.text(),
);
}).pipe(
Effect.catchTag("R2Error", (error) =>
Effect.succeed(
HttpServerResponse.text(error.message, { status: 500 }),
),
),
),
};
}).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
);

Three things make this different from a wrangler.toml binding:

  • The binding is declared where it’s used — deploying the Worker deploys the wiring.
  • The client is fully typed, and its errors (like R2Error) live in the Effect type system, so you can’t forget to handle them.
  • Access is scoped: request ReadBucket, WriteBucket, or ReadWriteBucket depending on what the Worker actually needs.

Every building block follows the same shape — see D1, Queues, and Hyperdrive, or walk through it step by step in tutorial part 2.

This is the Cloudflare shape of Schemaless RPC — see that page for the full concept.

Workers can call each other’s methods directly — no HTTP routes, no schema declarations, no public URL. Declare the Worker as a class whose type carries its RPC shape, and any method returning an Effect becomes callable from other Workers. This is the recommended way to do Worker-to-Worker RPC.

Define the callee with the class + .make() form (the class is a lightweight identifier; .make() provides the runtime, and bundlers tree-shake it out of consumers):

// src/WorkerB.ts — the tag carries the name + RPC shape
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
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* () {
return {
greet: (name: string) => Effect.succeed(`Hello ${name}`),
};
}),
);

Bind it from another Worker with Cloudflare.Workers.bindWorker and call its methods through the typed stub:

src/WorkerA.ts
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
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* () {
const greeting = yield* b.greet("world");
return HttpServerResponse.text(greeting);
}),
};
}),
) {}

bindWorker(WorkerB) registers a service binding on WorkerA, so each call travels over Cloudflare’s in-account service-binding fabric — never the public internet — and b.greet("world") is type-checked against WorkerB’s declared shape end-to-end.

The same pattern works for Durable Objects. For external clients across a trust boundary — where payloads need schema validation before they touch your code — reach for Effect RPC instead.

The workers.dev URL is for development. To serve a Worker from your own hostname, pass domain — a single hostname or an array. The zone is inferred from the hostname and must already exist in the account:

export default Cloudflare.Worker(
"Worker",
{
main: import.meta.url,
domain: "app.example.com",
},
// ...
);

For zone setup, DNS records, and route patterns, see the custom domains guide.

A Worker doesn’t have to be an Effect program. When main points at a plain module — a classic async fetch handler, or a prebuilt bundle from another tool — declare bindings with the env prop and derive the runtime type with InferEnv:

alchemy.run.ts
import * as Cloudflare from "alchemy/Cloudflare";
export const Bucket = Cloudflare.R2.Bucket("Bucket");
export const DB = Cloudflare.D1.Database("DB");
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { Bucket, DB },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;

InferEnv maps each env entry to its native workers-types client — an R2 bucket becomes R2Bucket, a D1 database becomes D1Database, a Durable Object becomes a typed DurableObjectNamespace, and Config/Redacted values become string. The handler stays plain JavaScript but the env can never drift from the infrastructure that produced it:

src/worker.ts
import type { WorkerEnv } from "../alchemy.run.ts";
export default {
async fetch(request: Request, env: WorkerEnv) {
const object = await env.Bucket.get("hello.txt");
return new Response(object ? await object.text() : "Not found");
},
};

Guides that build on Workers:

Related:

  • Durable Objects — stateful instances behind your Worker.
  • KV — low-latency key-value reads at the edge.
  • Queues — background work with at-least-once delivery.
  • Workflows — durable multi-step jobs that outlive a request.
  • Secrets & env — bind .env values and secrets into the Worker.
  • Domains — zones, DNS records, and certificates.

Reference: