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.

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