| 1 | --- |
| 2 | title: Runtime |
| 3 | description: A Runtime is a Resource that carries the code it runs — a Worker, Lambda Function, Container, or Server declared with the Effectful Constructor pattern. Bind what you need, return what you expose. |
| 4 | --- |
| 5 | |
| 6 | In Alchemy, a **Runtime** is a [Resource](/infrastructure-as-code/resource) |
| 7 | that carries the code it runs: a Cloudflare Worker, Lambda Function, |
| 8 | ECS Task, Container, or Server. The props are the cloud configuration. |
| 9 | The Effect is the code. Here is the running example from |
| 10 | [What is Alchemy?](/what-is-alchemy), which this page takes apart: |
| 11 | |
| 12 | ```typescript |
| 13 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 14 | import * as Effect from "effect/Effect"; |
| 15 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 16 | |
| 17 | export const Uploads = Cloudflare.R2.Bucket("Uploads"); |
| 18 | |
| 19 | export default Cloudflare.Worker( |
| 20 | "Api", |
| 21 | { main: import.meta.url }, |
| 22 | Effect.gen(function* () { |
| 23 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads); |
| 24 | |
| 25 | return { |
| 26 | fetch: Effect.gen(function* () { |
| 27 | const obj = yield* bucket.get("hello.txt"); |
| 28 | return obj |
| 29 | ? HttpServerResponse.text(yield* obj.text()) |
| 30 | : HttpServerResponse.text("Not found", { status: 404 }); |
| 31 | }), |
| 32 | }; |
| 33 | }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)), |
| 34 | ); |
| 35 | ``` |
| 36 | |
| 37 | One deploy ships both. `main: import.meta.url` tells Alchemy where the |
| 38 | code lives. At deploy time Alchemy bundles this file's default export, |
| 39 | which by convention is the Worker itself, and uploads that bundle as |
| 40 | the Worker's script. Most Runtimes need no other props, because |
| 41 | everything else they depend on comes in through bindings. |
| 42 | |
| 43 | The rest of this page follows that Effect. Alchemy also supports the |
| 44 | traditional two-file shape, a config file pointing at a plain |
| 45 | `async fetch` handler, compared in |
| 46 | [Effect handlers vs async handlers](#effect-handlers-vs-async-handlers). |
| 47 | |
| 48 | ## The Effectful Constructor |
| 49 | |
| 50 | Every Runtime is declared with the same Effect. It binds |
| 51 | the resources it needs, then returns the interface it exposes: |
| 52 | |
| 53 | ```typescript |
| 54 | Effect.gen(function* () { |
| 55 | // bind what you need |
| 56 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads); |
| 57 | |
| 58 | // return what you expose |
| 59 | return { |
| 60 | fetch: Effect.gen(function* () { |
| 61 | /* use bucket */ |
| 62 | }), |
| 63 | }; |
| 64 | }); |
| 65 | ``` |
| 66 | |
| 67 | This is the **Effectful Constructor**. The outer Effect is the |
| 68 | **Construction phase**, which runs at deploy time and again at cold |
| 69 | start. What it returns is the **Runtime phase**, which runs per |
| 70 | request inside the deployed Runtime. A Worker or Lambda Function |
| 71 | returns its handlers. A Durable Object returns its RPC methods. A |
| 72 | Workflow returns its run function. A |
| 73 | [Layer](/infrastructure-as-effects/layers) returns a service. Learn it |
| 74 | once and every Runtime in Alchemy is a variation on it. |
| 75 | |
| 76 | ## Fetch |
| 77 | |
| 78 | `fetch` serves HTTP. It is a per-request Effect. Yield |
| 79 | `HttpServerRequest` to read the request and return an |
| 80 | `HttpServerResponse`: |
| 81 | |
| 82 | ```typescript |
| 83 | import * as Effect from "effect/Effect"; |
| 84 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 85 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 86 | |
| 87 | Effect.gen(function* () { |
| 88 | return { |
| 89 | fetch: Effect.gen(function* () { |
| 90 | const request = yield* HttpServerRequest; |
| 91 | return HttpServerResponse.text(`hello from ${request.url}`); |
| 92 | }), |
| 93 | }; |
| 94 | }); |
| 95 | ``` |
| 96 | |
| 97 | It runs inside the deployed Runtime, once per request. |
| 98 | [Phases](/infrastructure-as-effects/phases) covers exactly when. |
| 99 | |
| 100 | ## RPC |
| 101 | |
| 102 | The interface isn't limited to `fetch`. Add methods that return an |
| 103 | `Effect` or a `Stream`, and any resource that binds this Worker calls |
| 104 | them through a typed client: |
| 105 | |
| 106 | ```typescript |
| 107 | export default class Greeter extends Cloudflare.Worker<Greeter>()( |
| 108 | "Greeter", |
| 109 | { main: import.meta.url }, |
| 110 | Effect.gen(function* () { |
| 111 | return { |
| 112 | greet: (name: string) => Effect.succeed(`hello ${name}`), |
| 113 | fetch: Effect.gen(function* () { |
| 114 | return HttpServerResponse.text("ok"); |
| 115 | }), |
| 116 | }; |
| 117 | }), |
| 118 | ) {} |
| 119 | ``` |
| 120 | |
| 121 | Another Worker binds `Greeter` and calls `greet` as if it were local: |
| 122 | |
| 123 | ```typescript |
| 124 | import Greeter from "./Greeter.ts"; |
| 125 | |
| 126 | export default Cloudflare.Worker( |
| 127 | "Api", |
| 128 | { main: import.meta.url }, |
| 129 | Effect.gen(function* () { |
| 130 | const greeter = yield* Cloudflare.Workers.bindWorker(Greeter); |
| 131 | |
| 132 | return { |
| 133 | fetch: Effect.gen(function* () { |
| 134 | return HttpServerResponse.text(yield* greeter.greet("world")); |
| 135 | }), |
| 136 | }; |
| 137 | }), |
| 138 | ); |
| 139 | ``` |
| 140 | |
| 141 | No schema, no runtime validation. Serialization is automatic and |
| 142 | `greeter.greet` is typed from the class. This is |
| 143 | [Schemaless RPC](/apis/schemaless), the default for internal calls. The |
| 144 | [APIs](/apis) section adds the schema'd surfaces for trust boundaries. |
| 145 | |
| 146 | ## Bindings |
| 147 | |
| 148 | The `yield*` lines at the top of the constructor are Bindings. Each one |
| 149 | connects a resource to this Runtime and hands back a client: |
| 150 | |
| 151 | ```typescript |
| 152 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads); |
| 153 | const kv = yield* Cloudflare.KV.ReadWriteNamespace(Sessions); |
| 154 | |
| 155 | // inside fetch |
| 156 | yield* bucket.put("key", "value"); |
| 157 | yield* kv.get("session-id"); |
| 158 | ``` |
| 159 | |
| 160 | A Cloudflare R2 binding and an AWS S3 binding look the same from here. |
| 161 | What differs is what they generate at deploy time: a native Worker |
| 162 | binding on one, an IAM statement and an environment variable on the |
| 163 | other. See [Bindings](/infrastructure-as-effects/binding) for how they |
| 164 | work. |
| 165 | |
| 166 | ## Layers |
| 167 | |
| 168 | A Binding is infrastructure the Runtime uses directly. A Layer |
| 169 | packages infrastructure behind a service of your own — it owns its |
| 170 | resources and bindings internally, and providing it on the |
| 171 | constructor is what brings them into the deploy: |
| 172 | |
| 173 | ```typescript |
| 174 | export default Cloudflare.Worker( |
| 175 | "Api", |
| 176 | { main: import.meta.url }, |
| 177 | Effect.gen(function* () { |
| 178 | const jobs = yield* JobService; // a service of your own |
| 179 | |
| 180 | return { |
| 181 | fetch: Effect.gen(function* () { |
| 182 | return HttpServerResponse.json(yield* jobs.getJob("job-1")); |
| 183 | }), |
| 184 | }; |
| 185 | }).pipe(Effect.provide(JobServiceKV)), // the Layer brings a KV Namespace with it |
| 186 | ); |
| 187 | ``` |
| 188 | |
| 189 | On the next deploy the Layer's namespace is created and bound to this |
| 190 | Worker, and the handler only ever sees `JobService`. Swap |
| 191 | `JobServiceKV` for a DynamoDB-backed Layer and the handler doesn't |
| 192 | change. This is the fundamental building block of Infrastructure as |
| 193 | Effects, and [Layers](/infrastructure-as-effects/layers) builds one |
| 194 | from scratch. |
| 195 | |
| 196 | ## Three ways to declare a Runtime |
| 197 | |
| 198 | #### Inline |
| 199 | |
| 200 | The inline form above is enough when nothing else needs to reference |
| 201 | the Worker by name: |
| 202 | |
| 203 | ```typescript |
| 204 | export default Cloudflare.Worker("Api", { main: import.meta.url }, effect); |
| 205 | ``` |
| 206 | |
| 207 | #### Class |
| 208 | |
| 209 | Wrap it in a class and the type becomes nominal. Hovers, errors, and |
| 210 | Stack outputs say `Api` instead of an anonymous shape: |
| 211 | |
| 212 | ```typescript |
| 213 | export default class Api extends Cloudflare.Worker<Api>()( |
| 214 | "Api", |
| 215 | { main: import.meta.url }, |
| 216 | effect, |
| 217 | ) {} |
| 218 | ``` |
| 219 | |
| 220 | #### Tag and Layer |
| 221 | |
| 222 | The third form splits identity from implementation. The class declares |
| 223 | only a **Tag**. `.make(props, effect)` produces the implementation |
| 224 | **Layer**: |
| 225 | |
| 226 | ```typescript |
| 227 | // src/Api.ts |
| 228 | export class Api extends Cloudflare.Worker<Api, {}>()("Api") {} |
| 229 | |
| 230 | export default Api.make( |
| 231 | { main: import.meta.url }, |
| 232 | Effect.gen(function* () { |
| 233 | /* the Effectful Constructor */ |
| 234 | }), |
| 235 | ); |
| 236 | ``` |
| 237 | |
| 238 | The Layer is provided to the Stack, not to another Worker: |
| 239 | |
| 240 | ```typescript |
| 241 | // alchemy.run.ts |
| 242 | import ApiLive, { Api } from "./src/Api.ts"; |
| 243 | |
| 244 | export default Alchemy.Stack( |
| 245 | "MyApp", |
| 246 | { providers: Cloudflare.providers(), state: Cloudflare.state() }, |
| 247 | Effect.gen(function* () { |
| 248 | const api = yield* Api; |
| 249 | return { url: api.url }; |
| 250 | }).pipe(Effect.provide(ApiLive)), |
| 251 | ); |
| 252 | ``` |
| 253 | |
| 254 | Other Runtimes bind to the Tag, never the implementation. Importing a |
| 255 | Tag pulls in no runtime code, so a Worker that binds `Api` doesn't |
| 256 | bundle `Api`'s implementation, which may not even target the same |
| 257 | runtime. One Tag can also have several implementations, each consumer |
| 258 | providing the one that fits. This split is what makes |
| 259 | [Circular Bindings](/infrastructure-as-effects/circular-bindings) |
| 260 | possible. |
| 261 | |
| 262 | ## Effect handlers vs async handlers |
| 263 | |
| 264 | The Effect style gives handlers typed errors, composable retries, and |
| 265 | bindings resolved through Effect's context: |
| 266 | |
| 267 | ```typescript |
| 268 | export default Cloudflare.Worker( |
| 269 | "Worker", |
| 270 | { main: import.meta.url }, |
| 271 | Effect.gen(function* () { |
| 272 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads); |
| 273 | return { |
| 274 | fetch: Effect.gen(function* () { |
| 275 | /* ... */ |
| 276 | }), |
| 277 | }; |
| 278 | }), |
| 279 | ); |
| 280 | ``` |
| 281 | |
| 282 | Because bindings are context, a handler can also depend on a service |
| 283 | of your own instead of a resource, with the storage swapped by |
| 284 | providing a different [Layer](#layers). |
| 285 | |
| 286 | The async style is a standard `async fetch`. Bindings go on the |
| 287 | resource's `env` prop and are typed with `InferEnv`: |
| 288 | |
| 289 | ```typescript |
| 290 | // alchemy.run.ts |
| 291 | export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 292 | |
| 293 | export const Worker = Cloudflare.Worker("Worker", { |
| 294 | main: "./src/worker.ts", |
| 295 | env: { Uploads }, |
| 296 | }); |
| 297 | ``` |
| 298 | |
| 299 | ```typescript |
| 300 | // src/worker.ts |
| 301 | import type { WorkerEnv } from "../alchemy.run.ts"; |
| 302 | |
| 303 | export default { |
| 304 | async fetch(request: Request, env: WorkerEnv) { |
| 305 | const obj = await env.Uploads.get("hello.txt"); |
| 306 | return obj |
| 307 | ? new Response(await obj.text()) |
| 308 | : new Response("Not found", { status: 404 }); |
| 309 | }, |
| 310 | }; |
| 311 | ``` |
| 312 | |
| 313 | There is no service boundary here. The handler reaches for `env.Uploads` |
| 314 | directly, so a different storage backend means a different handler. |
| 315 | |
| 316 | Both styles deploy through the same provider and produce the same |
| 317 | artifact. The Effect style unlocks [Layers](#layers), structured |
| 318 | retries, and fine-grained testing. The async style fits existing |
| 319 | handler code. |
| 320 | |
| 321 | ## Instance scope vs request scope |
| 322 | |
| 323 | The constructor runs once per instance: a Worker isolate, a Lambda |
| 324 | sandbox, a server process. Each event runs the handler in a fresh |
| 325 | `Scope`: |
| 326 | |
| 327 | ```typescript |
| 328 | Effect.gen(function* () { |
| 329 | // instance scope: runs once, reused by every event |
| 330 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads); |
| 331 | |
| 332 | return { |
| 333 | fetch: Effect.gen(function* () { |
| 334 | // request scope: runs per event, closed when the event settles |
| 335 | }), |
| 336 | }; |
| 337 | }); |
| 338 | ``` |
| 339 | |
| 340 | Build things at instance scope. Use things at request scope: |
| 341 | |
| 342 | ```typescript |
| 343 | Effect.gen(function* () { |
| 344 | const secret = yield* fetchSecret; |
| 345 | // ✓ one-shot I/O producing a plain value, cached for every event |
| 346 | |
| 347 | const conn = yield* acquireConnection; |
| 348 | // ✗ disposable. The instance scope may never close, so the |
| 349 | // connection may never be released |
| 350 | |
| 351 | return { |
| 352 | fetch: Effect.gen(function* () { |
| 353 | const conn = yield* acquireConnection; |
| 354 | // ✓ acquired per event, released when the event settles |
| 355 | }), |
| 356 | }; |
| 357 | }); |
| 358 | ``` |
| 359 | |
| 360 | Anything shared across calls within one event is memoized on the |
| 361 | request scope. `Drizzle.Postgres` opens its pool on the first query of |
| 362 | an event, every query in that event reuses it, and the pool closes when |
| 363 | the event settles. The |
| 364 | [SQL connection lifecycle](/sql/effect-sql/lifecycle) is the canonical |
| 365 | statement of this contract. |
| 366 | |
| 367 | A finalizer added inside a handler runs when the event settles: |
| 368 | |
| 369 | ```typescript |
| 370 | fetch: Effect.gen(function* () { |
| 371 | yield* Effect.addFinalizer(() => flushMetrics.pipe(Effect.ignore)); |
| 372 | return HttpServerResponse.text("ok"); |
| 373 | }), |
| 374 | ``` |
| 375 | |
| 376 | On workerd it runs after the response, via `ctx.waitUntil`. On Lambda |
| 377 | it runs before the response leaves, so keep it cheap and put anything |
| 378 | that must not be lost in the handler itself. |
| 379 | |
| 380 | Instance finalizers are best effort. On workerd they never run, because |
| 381 | isolates are frozen or evicted without a teardown hook. On Lambda they |
| 382 | get a 500 ms `SIGTERM` window at spin-down, skipped on hard failures. |
| 383 | On servers they run on graceful process exit. Design so nothing needs |
| 384 | instance-level cleanup. |
| 385 | |
| 386 | ## Where next |
| 387 | |
| 388 | - [Bindings](/infrastructure-as-effects/binding) — what |
| 389 | `yield* ReadWriteBucket(Uploads)` generates: IAM, env injection, a |
| 390 | typed client. |
| 391 | - [APIs](/apis) — how bound resources call the methods you return. |
| 392 | - [Phases](/infrastructure-as-effects/phases) — when the constructor |
| 393 | runs vs the interface it returns, and why. |
| 394 | - [Custom Runtime](/infrastructure-as-effects/custom-runtime) — bring |
| 395 | the Runtime model to a new compute target. |