# How workerd, the Cloudflare Workers runtime, is built

> I read the workerd source to explain its server, service graph, V8 isolates, JavaScript APIs, request contexts, and self-hosting model.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-09 | Updated: 2026-08-07 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/workerd/

Cloudflare Workers uses an open-source runtime named **workerd**.

workerd runs Worker code on the Cloudflare network. Wrangler also uses workerd for local development.

If Workers are new to you, start with [your first Cloudflare Worker](https://flaviocopes.com/cloudflare-workers/). My [free Cloudflare Workers course](https://flaviocopes.com/courses/cloudflare-workers/) takes you through a complete project.

You can run workerd on your server because Cloudflare publishes its source.

This post explains the path from a Hypertext Transfer Protocol (HTTP) request to JavaScript. It also explains the limits of self-hosting.

The first post explains [how Plausible Analytics Community Edition is built](https://flaviocopes.com/how-plausible-analytics-is-built/).

For this post, I went through the [workerd source](https://github.com/cloudflare/workerd) and followed one request from the server to JavaScript and back.

workerd changes frequently. Some file names and implementation details can change after this post.

The project uses the Apache 2.0 license.

This analysis covers only the open-source runtime. Cloudflare does not publish all its deployment and security systems in this repository.

That difference is important for self-hosting.

## Ten important design ideas

The workerd design gives us these important ideas:

- **Build the runtime for one primary task.** workerd is a server runtime. It is not a general command-line environment.
- **Make each capability explicit.** Configuration gives a Worker access to sockets, services, storage, networks, and secrets.
- **Use web standards at the application boundary.** Worker code receives a `Request` and returns a `Response`.
- **Create one context for each request.** One `IoContext` owns timers, subrequests, logs, limits, and background tasks.
- **Preserve streams across language boundaries.** Native request bodies become JavaScript streams without one large intermediate buffer.
- **Share expensive runtime components.** One runtime process can contain many V8 isolates.
- **Implement platform APIs in native code.** All isolates can use one native implementation of an API.
- **Store compatibility policy as data.** A date and flags select the behavior for each Worker.
- **Separate the runtime from the platform.** The repository does not include the full Cloudflare platform.
- **Assign self-hosting responsibilities clearly.** The operator must supply limits, isolation, updates, monitoring, and scaling.

## The purpose of workerd

[workerd](https://github.com/cloudflare/workerd) is a server runtime for JavaScript and WebAssembly. It uses the V8 JavaScript engine.

The final `d` follows a Unix naming convention. It identifies a server daemon.

workerd is not a web framework. It is also not a general replacement for [Node.js](https://flaviocopes.com/nodejs/).

The runtime does these tasks:

- It listens for events.
- It creates and manages V8 isolates.
- It gives web and Cloudflare APIs to JavaScript.
- It converts native HTTP data to `Request`, `Response`, and stream objects.
- It connects Worker code to configured services through bindings.
- It tracks the life of each request.
- It runs background work registered with `waitUntil()`.

A Worker can contain only this code:

```js
export default {
  async fetch(request, env, ctx) {
    return new Response('Hello from workerd')
  },
}
```

The code does not call `listen()`.

The runtime owns the server. The runtime calls the Worker when an event arrives.

This model differs from the usual Node.js server model.

## The main request path

An HTTP request moves through these runtime layers:

```mermaid
flowchart LR
  accTitle: How an HTTP request moves through workerd
  accDescr: A configured socket sends a request to a Worker service. An I/O context runs the handler in a V8 isolate and returns a streamed response.
  Config["Cap'n Proto config"] --> Socket["KJ HTTP socket"]
  Config --> Service["Named service"]
  Config --> Bindings["Bindings and outbound services"]
  Client["HTTP client"] --> Socket
  Socket --> Service
  Service --> Entry["WorkerEntrypoint"]
  Entry --> Context["IoContext"]
  Context --> Isolate["V8 isolate lock"]
  Isolate --> Handler["JavaScript fetch handler"]
  Bindings --> Context
  Handler --> Response["Response stream"]
  Response --> Client
```

The diagram uses names from the source.

The `server/` directory contains the binary and server setup. The `io/` directory contains Worker and request-lifetime code.

The `api/` directory contains JavaScript application programming interfaces (APIs). JavaScript Glue (JSG) connects C++ code to V8.

## The main source directories

The repository contains a runtime, an API library, and a server:

```text
workerd/
  docs/                  architecture and development documentation
  npm/                   packages for prebuilt workerd binaries
  samples/               runnable Worker configurations
  src/workerd/api/       web, Cloudflare, and Node-compatible APIs
  src/workerd/io/        isolates, requests, actors, I/O, and limits
  src/workerd/jsg/       C++ and JavaScript binding layer
  src/workerd/server/    CLI, config, sockets, and service orchestration
  src/workerd/util/      shared runtime utilities
```

Most of the source is C++.

V8 runs the JavaScript code. KJ supplies many asynchronous C++ components around V8.

KJ is part of the Cap'n Proto project. workerd uses KJ for promises, HTTP, streams, networking, and object ownership.

The repository also contains JavaScript, TypeScript, Rust, and Python.

## Configuration defines the service graph

Most application servers start from application code. workerd starts from a [Cap'n Proto configuration](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/server/workerd.capnp).

This example defines one Worker and one HTTP socket:

```text
using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "main", worker = .mainWorker),
  ],
  sockets = [
    (
      name = "http",
      address = "*:8080",
      http = (),
      service = "main"
    ),
  ]
);

const mainWorker :Workerd.Worker = (
  modules = [
    (
      name = "worker.js",
      esModule = embed "worker.js"
    ),
  ],
  compatibilityDate = "2026-06-01"
);
```

This file defines these items:

1. A Worker named `main`.
2. An HTTP socket on port 8080.
3. A route from the socket to the Worker.

The JavaScript code does not select the port. The configuration selects the port.

### A service can have different implementations

The [`Service` schema](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/server/workerd.capnp) supports different destination types:

- A Worker.
- A network.
- An external server.
- A directory that workerd exposes as an HTTP service.

A socket gives an external client access to a service. A binding gives another Worker access to a service.

This model supports incoming requests, Worker calls, files, internal backends, and outbound network access.

### Bindings give capabilities to a Worker

The `bindings` list controls the values in `env`.

A binding can contain text, JavaScript Object Notation (JSON), binary data, a key, a service, or a storage interface.

This example gives a Worker access to a service named `api`:

```text
bindings = [
  (name = "API", service = "api"),
]
```

The Worker calls the service through the binding:

```js
const response = await env.API.fetch('https://api/users')
```

The hostname does not select the destination. The binding selects the destination.

The `api` service can be another Worker in the same process. This call does not require a network socket.

Cloudflare calls this type of service a **nanoservice**.

### Global `fetch()` also uses a capability

workerd creates an `internet` service by default. This service supports the global `fetch()` function.

The default network policy permits public addresses. It blocks private addresses.

This policy helps to prevent server-side request forgery (SSRF).

User input cannot make the default service contact a private backend. The operator can define an explicit service binding for that backend.

The operator can change the default policy. The configuration makes this security boundary visible.

## The workerd start sequence

The command-line entry point is in [`workerd.c++`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/server/workerd.c%2B%2B).

The `serve` command loads the configuration. It then initializes V8, creates a `Server`, and calls `Server::run()`.

The [`Server::run()` method](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/server/server.c%2B%2B) has two main phases:

```text
startServices()
listenOnSockets()
```

workerd builds the internal service graph before it accepts traffic.

The first phase creates Workers, networks, external servers, disk services, bindings, actor namespaces, and tail workers. It also validates service references.

The second phase opens the configured sockets. Each socket sends its traffic to a selected service.

### One process contains one V8 system

The [JSG documentation](https://github.com/cloudflare/workerd/blob/v1.20260611.1/docs/jsg.md) describes a process-wide `V8System`.

workerd creates this system one time. Individual Worker isolates live in the system.

Each isolate has a separate JavaScript heap. An isolate cannot directly share JavaScript objects with another isolate.

This design uses fewer resources than one operating-system process for each Worker.

## JSG gives native APIs to JavaScript

V8 implements JavaScript. It does not implement Cloudflare runtime APIs.

For example, V8 does not supply `KVNamespace`, `R2Bucket`, `DurableObjectState`, or `ExecutionContext`.

workerd implements these APIs in C++. JavaScript Glue (JSG) exposes the APIs to JavaScript.

The [`WorkerdApi`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/server/workerd-api.c%2B%2B) declares the types that an isolate can use.

The API contains these groups:

- Global-scope APIs.
- HTTP and stream APIs.
- Web Crypto.
- WebSockets and sockets.
- Cache, KV, and R2.
- Durable Objects and queues.
- Structured Query Language (SQL) interfaces.
- Node.js compatibility.
- Tracing.
- WebAssembly and Python support.

JSG converts values between C++ and JavaScript. It can expose a C++ resource or method as a JavaScript object or method.

A native stream can become a JavaScript `ReadableStream`.

Native implementations also reduce duplicate code. workerd does not load a separate JavaScript implementation of each API into every isolate.

## Compatibility dates select runtime behavior

Each Worker configuration contains a compatibility date:

```text
compatibilityDate = "2026-06-01"
```

This value controls runtime behavior. It is not only deployment metadata.

The [`compileCompatibilityFlags()` function](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/io/compatibility-date.c%2B%2B) reads the date and optional flags.

The function produces the behavior flags for one Worker:

```text
compatibility date
      +
explicit enable and disable flags
      |
      v
compiled runtime behavior flags
      |
      v
API surface and semantics for this isolate
```

The runtime rejects a date that is newer than the runtime. It also rejects a date in the future.

A new workerd version can still supply old behavior to an older Worker.

This system lets Cloudflare update V8 without changing all applications on the same date.

A workerd version number also includes its release date and a revision number.

## workerd prepares the Worker script

The main script code is in [`worker.c++`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/io/worker.c%2B%2B).

A `Worker::Isolate` owns the V8 isolate and runtime API. A `Worker::Script` owns the compiled application in that isolate.

workerd takes the isolate lock and creates a V8 context during script construction.

For an ECMAScript (ES) module Worker, workerd loads modules and resolves imports. It compiles the main module and finds the exported handlers.

For Service Worker syntax, workerd compiles one global script. The runtime later sends events to the registered handlers.

The ES module syntax is the current model:

```js
export default {
  async fetch(request, env, ctx) {
    return new Response('Hello')
  },
}
```

workerd prepares the script before it receives a normal request.

## The path of one HTTP request

Assume that a client sends this request:

```bash
curl http://localhost:8080/hello
```

The request first reaches the configured `http` socket.

### KJ accepts the request

The HTTP server code is in [`server.c++`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/server/server.c%2B%2B).

The connection implements the KJ `HttpService` interface.

Its `request()` method receives these values:

- The HTTP method.
- The URL.
- Native headers.
- An asynchronous request-body stream.
- A native response object.

The server can change request metadata and headers when the configuration requires a change.

The server then asks the selected service to start a request.

### The service creates a Worker entrypoint

For a Worker service, `WorkerService::startRequest()` creates a [`WorkerEntrypoint`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/io/worker-entrypoint.c%2B%2B).

This object connects the generic HTTP server to one Worker export.

It also creates a request observer. It creates configured tail workers for logs and traces.

JavaScript has not run at this point. The server still has native HTTP values.

### Each request gets one `IoContext`

`WorkerEntrypoint::init()` creates an [`IoContext`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/io/io-context.c%2B%2B) for a stateless request.

The `IoContext` contains the state for the active request:

- A Worker reference.
- Outbound channels and bindings.
- Timers and subrequests.
- Abort state.
- Logs and traces.
- `waitUntil()` tasks.
- Request metrics.
- Resource-limit hooks.
- Native input/output objects that JavaScript can reach.

The context creates an important ownership boundary.

An input/output object from one request must not operate in a different request. workerd connects each native resource to its `IoContext`.

The runtime reports an error if code uses a resource outside its owning context.

### The request waits for the isolate lock

One V8 isolate cannot run JavaScript on two threads at the same time.

`WorkerEntrypoint::request()` calls `context.run()`.

The context waits for a fair asynchronous lock. It then takes the V8 isolate lock and enters the correct JavaScript context.

workerd can now operate on JavaScript objects.

KJ promises and JSG connect asynchronous native operations to JavaScript promises. A continuation returns to the correct context after an operation finishes.

### Native HTTP becomes web-standard JavaScript

The entrypoint calls [`ServiceWorkerGlobalScope::request()`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/api/global-scope.c%2B%2B).

This method converts native HTTP data to JavaScript data.

It creates JavaScript `Headers`. It also creates a `Request` with the method, URL, metadata, abort signal, and body.

workerd does not automatically put the complete request body into memory.

The runtime wraps the native input stream in a JavaScript `ReadableStream`.

This Worker returns the input stream as the response body:

```js
export default {
  async fetch(request) {
    return new Response(request.body)
  },
}
```

The runtime does not need one large JavaScript string or `ArrayBuffer` for the complete body.

### workerd calls the exported handler

For an ES module Worker, the runtime calls the exported `fetch` handler with three values:

```text
request, env, ctx
```

`request` contains the JavaScript request data. `env` contains configured bindings.

`ctx` contains request controls. These controls include `waitUntil()` and `passThroughOnException()`.

The handler can return a `Response` or a `Promise<Response>`.

### The response returns to C++

workerd verifies that the result is a valid `Response`.

The runtime writes the status, headers, and body to the KJ response interface.

A JavaScript `ReadableStream` can continue to produce data after the handler returns the `Response` object.

The native HTTP server sends each data part to the client.

### A client disconnect causes an abort

The runtime monitors the client connection.

workerd can activate the request `AbortSignal` if the client disconnects before the response finishes.

The runtime also prevents use of an expired native request stream. The source calls this operation **neutering**.

JavaScript sees a simple `Request` object. A native socket with a shorter life exists below that object.

## The function of `waitUntil()`

A Worker can return a response and continue a separate task:

```js
export default {
  async fetch(request, env, ctx) {
    ctx.waitUntil(env.LOGS.fetch('https://logs/write'))

    return new Response('ok')
  },
}
```

The [`ExecutionContext::waitUntil()` implementation](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/api/global-scope.c%2B%2B) adds the promise to the current `IoContext`.

After the response finishes, `WorkerEntrypoint` drains the remaining tasks for the request.

The response does not wait for the logging request. The request context waits for it.

`waitUntil()` does not provide a durable job queue. A process failure can remove an unfinished task.

Use [Cloudflare Queues](https://flaviocopes.com/cloudflare-queues/) when the work must survive failures and retry safely.

## Subrequests use configured channels

Global `fetch()` and binding `fetch()` calls do not create a Node.js socket directly.

Each call asks the current `IoContext` for an outbound channel. A channel number identifies the configured destination.

Global `fetch()` usually uses the `internet` service. `env.API.fetch()` uses the service connected to the `API` binding.

The destination can be one of these service types:

- The public network.
- A fixed external server.
- Another Worker.
- A storage implementation with the required binding protocol.
- A disk service.

The JavaScript interface does not change. The configuration selects the implementation.

This design also supports tests. A test configuration can replace a production backend with a small mock Worker.

## Same-process services remove a network operation

Microservices give independent service boundaries. Network calls also add serialization, routing, authentication, retries, timeouts, and failure conditions.

workerd can run several Workers in one process. Service bindings connect these Workers.

The caller uses a service interface. workerd can deliver the request in the same process and thread.

The [original workerd announcement](https://blog.cloudflare.com/workerd-open-source-workers-runtime/) calls this design a nanoservice model.

Too many service boundaries can still make an application difficult to understand. workerd only reduces the runtime cost of each boundary.

## workerd and Node.js have different host models

Node.js usually gives JavaScript broad access to the operating system.

A Node.js application can open a port, read files, start child processes, and load native modules.

workerd uses the opposite model. The host creates the server and gives selected capabilities to the Worker.

The default API uses web standards:

- `Request`.
- `Response`.
- `fetch()`.
- `URL`.
- Web Crypto.
- Web Streams.
- WebSockets.

workerd also implements many Node.js compatibility APIs. The `nodejs_compat` flag adds compatible APIs and modules.

This flag does not create a normal Node.js process.

Some packages require unrestricted files, native addons, child processes, or a global server. These packages can fail or have different behavior.

The workerd programming model remains event-based and capability-based.

## Wrangler uses workerd locally

If you have not used the tool yet, start with my [Wrangler guide](https://flaviocopes.com/cloudflare-wrangler/).

This command starts a local Workers environment:

```bash
npx wrangler dev
```

[Miniflare](https://github.com/cloudflare/workers-sdk/tree/main/packages/miniflare) prepares the environment. workerd runs the Worker code.

Miniflare supplies local implementations for services such as D1, KV, R2, Queues, Durable Objects, and the Cache API.

The local architecture has this sequence:

```text
Wrangler
  -> Miniflare builds the local service graph
  -> workerd executes Worker code
  -> local services implement development bindings
```

workerd supplies the runtime and binding interfaces. Miniflare supplies much of the local development platform.

I use this path when I [run Cloudflare D1 locally](https://flaviocopes.com/run-cloudflare-d1-locally/).

This design gives good runtime parity. It does not copy the complete Cloudflare platform to a local computer.

## Direct operation of workerd

Wrangler is convenient, but workerd does not require Wrangler.

Create a project and install workerd:

```bash
mkdir workerd-demo
cd workerd-demo
npm init -y
npm install --save-dev workerd
```

Create `worker.js` with this content:

```js
export default {
  async fetch(request) {
    const url = new URL(request.url)

    return Response.json({
      message: 'Hello from workerd',
      path: url.pathname,
    })
  },
}
```

Use the `config.capnp` file from the earlier example. Then start the server:

```bash
npx workerd serve config.capnp
```

Send a request from a different terminal:

```bash
curl http://localhost:8080/hello
```

The server returns this response:

```json
{"message":"Hello from workerd","path":"/hello"}
```

This example does not require a Cloudflare account. It does not deploy code to the Cloudflare network.

## A self-contained executable

The `serve` command reads configuration and source files during startup.

The `compile` command puts those files in one executable:

```bash
npx workerd compile config.capnp > workerd-demo
chmod +x workerd-demo
```

The output contains these components:

- The workerd runtime.
- The encoded configuration.
- The embedded Worker modules and data.

Run the executable:

```bash
./workerd-demo
```

The application no longer needs `worker.js`, `config.capnp`, Node.js, or `node_modules` during operation.

The executable depends on its operating system and CPU architecture. Build an executable for each deployment target.

## Production self-hosting

You can self-host workerd in production.

The [workerd README](https://github.com/cloudflare/workerd/tree/v1.20260611.1) describes workerd as an application server for self-hosted Worker applications.

The repository includes a production example with `systemd` socket activation.

In that example, `systemd` owns ports 80 and 443. It starts workerd as an unprivileged user and restarts a failed process.

You can also run workerd in a virtual machine or a container system.

Use an exact workerd version in production. Do not download an unspecified current version during each start.

A production system also needs these components:

- Process supervision.
- A secure Transport Layer Security (TLS) and load-balancing layer.
- Structured logs and monitoring.
- Operating-system resource limits.
- Health checks.
- A tested update and rollback process.
- Backups for persistent local data.

workerd is the application server in this system. The operator supplies the platform functions.

On the managed platform, [Workers observability](https://flaviocopes.com/cloudflare-workers-observability/) gives you logs and traces. A self-hosted setup needs its own equivalent.

## Storage in a self-hosted system

The workerd configuration supports interfaces for KV, R2, Queues, Analytics Engine, Hyperdrive, and other Cloudflare services.

The standalone binary does not contain the managed Cloudflare storage systems.

Many bindings send API operations to a configured service. The operator must supply a service that implements the required protocol.

Durable Objects have different local storage options.

workerd can run local Durable Objects with memory storage. It also has an experimental disk mode that uses SQLite files.

My [Durable Objects guide](https://flaviocopes.com/cloudflare-durable-objects/) explains how the managed version handles state and storage.

The configuration source states that each object stays local to one runtime instance.

workerd does not distribute these objects across a cluster.

Memory storage loses data when the process stops. Disk storage survives a restart, but the mode is experimental and instance-local.

Miniflare provides more development implementations. One local process still does not provide Cloudflare distributed storage.

## The standalone server does not apply Cloudflare limits

The runtime defines limit interfaces in [`limit-enforcer.h`](https://github.com/cloudflare/workerd/blob/v1.20260611.1/src/workerd/io/limit-enforcer.h).

These interfaces can control the following resources:

- JavaScript central processing unit (CPU) time.
- Isolate memory.
- Startup execution.
- Subrequest counts.
- KV operations.
- Buffer sizes.
- Background drain time.
- SQLite memory.

The Cloudflare platform can connect its limit systems to these interfaces.

The standalone server uses `NullIsolateLimitEnforcer`. Its per-request enforcer also does not apply limits.

The standalone runtime does not know the machine size, tenant model, billing policy, or orchestration system.

Do not assume that self-hosted workerd applies Cloudflare CPU, memory, or subrequest limits.

Use operating-system controls, virtual machines, containers, or another supervisor to limit the process.

## workerd is not a complete hostile-code sandbox

V8 isolates separate JavaScript heaps. This separation is not sufficient for arbitrary hostile code.

The workerd README gives this warning. The repository does not include all Cloudflare security layers.

A defect in V8, workerd, the operating system, or the CPU can cross an isolate boundary.

Run untrusted user code inside a stronger sandbox. A correctly configured virtual machine can provide one part of that system.

Do not assume that a normal container is a complete security boundary for hostile code.

This warning does not have the same effect on trusted application code.

Running your own Worker is a normal server deployment. Running untrusted user programs requires a security platform.

## workerd is not the complete Cloudflare platform

workerd is an important component of Cloudflare Workers. It is not the complete product.

One self-hosted workerd server does not supply these Cloudflare functions:

- The global Cloudflare network.
- Deployment to many locations.
- Routing between those locations.
- Automatic scaling.
- The Workers control plane.
- Managed KV, D1, R2, Queues, and other services.
- Multi-tenant security layers.
- Per-request resource limits.
- Managed monitoring and billing.
- Automatic security updates for the runtime.

The following table shows the difference:

| Open-source workerd | Managed Cloudflare Workers |
| --- | --- |
| JavaScript and WebAssembly runtime | Runtime and global hosting platform |
| V8 isolates and Worker APIs | Additional multi-tenant security layers |
| Configured sockets and services | Global traffic routing |
| Local process life cycle | Deployment and orchestration control plane |
| Binding interfaces | Managed storage implementations |
| Standalone limit hooks without enforcement | Cloudflare resource policies and metering |
| Infrastructure operated by you | Infrastructure operated by Cloudflare |

The open-source runtime gives application-code portability. It does not give automatic platform portability.

A Worker can depend on D1, R2, Queues, Durable Objects, or Artificial Intelligence (AI) services.

For that Worker, code migration is only one task. Data, service, and operations migrations are also necessary.

These platform services form the main lock-in boundary.

## Failure and behavior boundaries

An architecture analysis must include failure conditions.

### `waitUntil()` tasks are not durable

The runtime keeps each background task in the request context.

A normal stop can drain these tasks. A sudden process failure can remove them.

Use a durable queue for work that must survive a process failure.

### Memory storage disappears

Memory storage for a Durable Object exists only during the workerd process life.

Local disk storage persists data, but it stays with one runtime instance.

### One Worker can use all machine resources

The standalone limit enforcers do not stop a trusted Worker from excessive CPU or memory use.

The operating environment must apply these limits.

### A compatibility date does not freeze dependencies

The compatibility system keeps selected Worker API behavior.

The operator must still update workerd for V8 and runtime security corrections. Native dependencies and operating systems also change.

### Local parity has limits

Wrangler uses workerd, so local JavaScript behavior closely matches production behavior.

Local storage services are simulations or separate implementations. A local computer does not reproduce global routing, placement, or distributed failures.

## Reasons for the architecture

workerd separates the system into clear layers:

```text
Cap'n Proto config   declares services and capabilities
server               opens sockets and builds the service graph
WorkerService        selects a Worker and entrypoint
WorkerEntrypoint     connects an event to one Worker execution
IoContext            owns request I/O and background work
Worker::Isolate      owns the V8 execution environment
JSG                  exposes native APIs to JavaScript
GlobalScope          converts HTTP and calls the handler
KJ                   carries asynchronous I/O and streams
```

Each layer answers one type of question.

The service graph selects the request destination. Bindings select the capabilities for a Worker.

Compatibility flags select runtime behavior. `IoContext` selects the owner of each native resource.

JSG defines the JavaScript view of C++ APIs. KJ moves the response to the socket.

These boundaries correspond to important runtime risks.

Configuration prevents accidental access. Isolates separate JavaScript heaps.

Context ownership prevents resource use across requests. Compatibility dates prevent unplanned behavior changes.

## Ideas for other applications

The workerd design gives useful patterns for smaller systems:

- **Show each capability.** Pass database, email, and storage clients to code explicitly.
- **Create one request context.** Put logs, trace identifiers, abort signals, deadlines, and background tasks in one object.
- **Use standard request and response types.** Standard types make tests and migrations easier.
- **Preserve streams.** Do not buffer a body unless the application needs the complete value.
- **Separate an interface from its implementation.** Tests and production can use different storage implementations.
- **Version behavior deliberately.** Make each important behavior selection explicit.
- **Separate runtime safety from platform safety.** A language isolate is only one security layer.
- **Include operations in the design.** Limits, restarts, logs, updates, and data placement are product requirements.

## A runtime controls boundaries around the engine

workerd contains V8 and an HTTP server. That description does not explain the complete system.

The service graph controls reachability. Bindings control capabilities.

JSG controls the native APIs that enter JavaScript. The isolate lock controls execution.

`IoContext` controls native-resource life. Compatibility dates control behavior changes.

The `fetch()` handler stays small because the runtime manages these boundaries.

A server runtime does more than run JavaScript. It defines the meaning of JavaScript in an asynchronous server process.

## A small educational version

A small learning project does not need Web Crypto, Durable Objects, or Node.js compatibility.

The first version can contain one process and one JavaScript module.

### One process and one handler

The server converts an incoming request to a standard `Request`. It calls an exported `fetch()` handler.

The server then converts the returned `Response` to native HTTP data.

The first version does not need multiple isolates, a service graph, or dynamic Worker loading.

### One explicit environment object

The `env` object contains only the required capabilities:

```js
const env = {
  DATABASE: databaseClient,
  EMAIL: emailClient,
}
```

This object uses the binding concept without a configuration language.

### One request context

The `ctx` object contains these values:

- An abort signal.
- A logger.
- A trace identifier.
- A list of `waitUntil()` promises.

After the response, the server drains the promises for a limited time.

### Streaming request and response bodies

The server keeps the request and response bodies as web streams.

The first version does not put each upload or download into one memory buffer.

### Isolation for a real security requirement

One JavaScript process is sufficient when the server runs only trusted code.

A service that runs code from several users has a different requirement. That service needs a strong sandbox before it needs more APIs.

The first design has this structure:

```mermaid
flowchart LR
  accTitle: A small educational Worker runtime
  accDescr: An HTTP server creates standard web objects and calls one JavaScript handler with explicit environment and context objects.
  Client["HTTP client"] --> Server["HTTP server"]
  Server --> Request["Web Request"]
  Request --> Handler["JavaScript fetch handler"]
  Env["Explicit env capabilities"] --> Handler
  Context["Abort, logs, waitUntil"] --> Handler
  Handler --> Response["Web Response"]
  Response --> Client
```

Start with one process, one handler, explicit capabilities, one request context, and streams.

Add a service graph when the system needs independently configured services.

Add compatibility versions when different API behaviors must operate at the same time.

Add isolated execution when one application can block another application.

Add a hardened sandbox and resource limits before users can supply code.

workerd shows the result of these decisions after many years of production use.

Its architecture is a useful reference. It is not a mandatory checklist.
