Skip to content

Functions & Servers

Cloudflare Workers, AWS Lambda Functions, ECS Tasks, and Containers are Resources that ship runtime code along with their infrastructure. When you declare one you describe both:

  • The cloud configuration (memory, region, compatibility flags…)
  • The Effect that runs inside it

Both deploy together as one resource.

Every Function and Server is declared with the same shape — an Effect that declares its Bindings, then returns its interface:

Effect.gen(function* () {
// 1. Declare Bindings — the resources this program uses.
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
// 2. Return the interface — the Effects and functions it exposes.
return {
fetch: Effect.gen(function* () {
/* use bucket */
}),
};
}); // + Effect.provide(<the binding Layer>) — see Bindings

This is the Effectful Constructor — one simple, repeatable pattern you’ll use everywhere across alchemy:

  • A Worker or Lambda Function returns its handlers (fetch).
  • A Durable Object returns its RPC methods.
  • A Workflow returns its run function.
  • A Layer returns a service interface.

Bind what you need, return what you expose. Learn it once and every runtime in alchemy is a variation on it.

fetch is the interface member that serves HTTP — a per-request Effect. Yield HttpServerRequest to read the request; return an HttpServerResponse:

import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
Effect.gen(function* () {
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
return HttpServerResponse.text(`hello from ${request.url}`);
}),
};
});

Every Worker and Lambda Function serves HTTP through this member — it runs per request, inside the deployed Function (Phases covers exactly when).

The interface isn’t limited to fetch — add methods returning Effect or Stream, and any resource that binds this Worker calls them through a typed client:

export default class Greeter extends Cloudflare.Worker<Greeter>()(
"Greeter",
{ main: import.meta.url },
Effect.gen(function* () {
return {
greet: (name: string) => Effect.succeed(`hello ${name}`),
fetch: Effect.gen(function* () {
return HttpServerResponse.text("ok");
}),
};
}),
) {}

No schema, no runtime validation — serialization is automatic and the client type is inferred from the class. This is Schemaless RPC, the default for internal calls; the APIs section covers it plus the schema’d surfaces (Effect RPC, Effect HTTP) for trust boundaries.

Cloud configuration, a Binding, and a fetch handler together:

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(
"Api",
{
main: import.meta.url,
compatibility: { date: "2026-01-28" },
observability: { enabled: true },
},
Effect.gen(function* () {
// Init: bind a resource. The binding is the typed SDK.
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
return {
// Runtime: per-request handler.
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.provide(Cloudflare.R2.ReadWriteBucketBinding)),
);

The props (compatibility, observability, memory, routes…) are the cloud configuration; the Effect is the Effectful Constructor. One deploy ships both.

Inline — no class, the form used above. Fine when nothing else needs to reference the Worker by name:

export default Cloudflare.Worker("Api", { main: import.meta.url }, effect);

Inline class — the same declaration, but the class name makes the type nominal instead of structural: hovers, errors, and Stack outputs say Api instead of an anonymous shape:

export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
effect,
) {}

Tagged — split identity from implementation. The class declares only the Tag; .make(props, effect) produces the implementation Layer:

// src/Api.ts — the Tag: a lightweight identifier others bind to
export class Api extends Cloudflare.Worker<Api, {}>()("Api") {}
// same file, or a separate one — the implementation Layer
export default Api.make(
{ main: import.meta.url },
Effect.gen(function* () {
/* the Effectful Constructor */
}),
);

The implementation Layer is provided to the Stack, not to another Worker:

alchemy.run.ts
import ApiLive, { Api } from "./src/Api.ts";
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
Effect.gen(function* () {
const api = yield* Api;
return { url: api.url };
}).pipe(Effect.provide(ApiLive)),
);

The split matters because other Functions bind to the Tag — never the implementation. Importing a Tag pulls in no runtime code, so a Worker binding to Api doesn’t bundle Api’s implementation — a piece of code with its own dependencies that may not even target the same runtime (workerd vs node vs bun, serverless vs serverful). It also allows multiple implementations of one Tag: ship the Tag, and let each consumer provide the Layer that fits. This split is what makes Circular Bindings possible.

The yield* ReadWriteBucket(Bucket) line above is a binding — it connects a resource to the Function’s runtime:

const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
const kv = yield* Cloudflare.KV.ReadWriteNamespace(Sessions);
// Inside fetch:
yield* bucket.put("key", "value");
yield* kv.get("session-id");

Every binding obeys the same shape across providers — a Cloudflare R2 binding and an AWS S3 binding are interchangeable from the caller’s point of view. Every binding is a contract (the yield*) plus an interchangeable implementation Layer (the Effect.provide) — Bindings covers the deploy-time mechanics.

Workers and Lambda Functions support two styles for the runtime code. Both deploy through the same provider and produce the same artifact.

Effect style — handlers are Effects, with typed errors, composable retries, structured concurrency, and bindings resolved through Effect’s context:

export default Cloudflare.Worker(
"Worker",
{ main: import.meta.url },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
return {
fetch: Effect.gen(function* () {
/* ... */
}),
};
}),
);

Async style — handlers are standard async fetch functions. Bindings are declared on the resource’s env prop and typed via InferEnv:

alchemy.run.ts
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { Bucket },
});
src/worker.ts
import type { WorkerEnv } from "../alchemy.run.ts";
export default {
async fetch(request: Request, env: WorkerEnv) {
const object = await env.Bucket.get("key");
return new Response(object?.body ?? null);
},
};

Pick whichever fits your team. The Effect style unlocks Layers, structured retries, and fine-grained testing; the async style integrates better with existing handler code.

The Effectful Constructor splits every Function and Server into two lifetimes, and each has its own Scope:

ScopeCoversCloses
Instance scopethe constructor (init) and its Layersworkerd: never · Lambda: 500 ms at shutdown · servers: on exit
Request scopeone event — a request, RPC call, run, invocationwhen the event settles

The constructor runs once per instance — a Worker isolate, a Lambda sandbox, a server process. Everything it builds (bindings, SDK clients, Layers) is assembled once and reused by every event. One-shot I/O that produces a plain value is fine here — fetching a secret and caching it for a client, reading config — that’s exactly what once-per-instance is for. What doesn’t belong at init is anything disposable: connections, pools, streams, or any resource with a finalizer. The instance scope closes at shutdown at best (never on workerd), so cleanup registered here is best-effort or never — and on workerd, I/O objects (sockets, response bodies) are pinned to the request that created them and can’t be reused across events anyway.

Each event then runs your handler with a fresh Scope. Anything disposable belongs here — acquired lazily, released when the event settles:

fetch: Effect.gen(function* () {
// workerd: runs after the response via ctx.waitUntil, without
// blocking it. Lambda: runs before the response — keep it cheap.
yield* Effect.addFinalizer(() => releaseThings().pipe(Effect.ignore));
return HttpServerResponse.text("ok");
}),

When request finalizers run differs by runtime. workerd closes the scope into ctx.waitUntil, so they run after the response without blocking it. Lambda settles the scope inline, before the response leaves — a buffered invocation’s response isn’t released until the Invoke phase completes, and no deferral scheme (extension windows, dangling promises across freezes) is reliable, so the contract is blocking and honest: keep Lambda request finalizers cheap, and write anything that must not be lost durably inside the handler instead of flushing it from a finalizer.

Resources that should live for the whole event but be shared across calls within it — a database pool, for example — are memoized on the request scope. Drizzle.postgres opens its pool on the first query of an event and closes it when the event’s scope does; every query in that event reuses it.

workerd (Workers, Durable Objects, Workflows) has no teardown hook. The platform freezes or evicts an isolate without warning, so the instance scope exists but is never closed — a finalizer registered during init will never run. This is a platform constraint, not an alchemy choice: it’s the same reason a module-level setInterval or open socket in a plain Worker never gets cleaned up.

Lambda grants a brief Shutdown phase — because alchemy asks for one. A sandbox with no registered extensions is killed with no signal at all; alchemy’s generated entry registers an internal extension with the Extensions API, which makes Lambda send SIGTERM and allow 500 ms before SIGKILL. The entry closes the instance scope in that window, so init-level finalizers run at sandbox spin-down — best-effort and fast: half a second, not delivered on hard failures (a timeout reset kills the runtime without the signal). Flush caches and close connections here; never depend on it for critical writes.

Servers (Containers, ECS Tasks) are real processes. The program runs under a root scope that closes when the process shuts down gracefully, so instance-scoped resources — a connection kept warm across requests, a background consumer — are released on exit. (A hard kill still skips finalizers, as it would in any process.)

The rule of thumb:

  • Instance scope — build things. Cheap, reusable, no required cleanup.
  • Request scope — use things. I/O, connections, finalizers.
  • Instance-level cleanup is best-effort at most — 500 ms on Lambda, graceful-exit-only on servers, nonexistent on workerd. Design so nothing needs it.
  • Bindings — the deploy-time mechanics behind yield* ReadWriteBucket(Bucket): IAM, env injection, typed SDKs.
  • RPC — how bound resources call the methods you return.
  • Phases — when the constructor runs vs the interface it returns, and why.
  • Custom Runtime — bring the Functions & Servers model to a new compute target.