Navigating the workerd Codebase: Architecture Overview and Directory Map
Prerequisites
- ›Familiarity with C++ at an introductory level
- ›Basic understanding of JavaScript runtimes (Node.js or Deno is sufficient)
- ›Awareness of what Cloudflare Workers is as a product
Navigating the workerd Codebase: Architecture Overview and Directory Map
Cloudflare Workers run on millions of servers across 300+ cities, handling billions of requests every day. The runtime that makes this possible — workerd (pronounced "worker-dee") — was open-sourced in September 2022, giving the world access to the exact same V8-based C++ engine that powers one of the largest edge computing platforms on earth. Whether you want to self-host Workers, contribute to the project, or simply understand how a production-grade JavaScript runtime works from the inside, this series will take you through every layer.
This first article gives you the map. By the end, you'll understand the directory structure, the layered architecture, the three foundational design principles, and how to run a minimal "Hello World" worker locally. Every subsequent article builds on this foundation.
What is workerd?
At its core, workerd is a server-side JavaScript and WebAssembly runtime built on Google's V8 engine. It is not a general-purpose Node.js replacement. It is purpose-built for the server use case: receiving HTTP requests, running isolated JavaScript handlers, and returning responses — all with millisecond cold starts.
From the README.md:
workerd(pronounced: "worker-dee") is a JavaScript / Wasm server runtime based on the same code that powers Cloudflare Workers.
The project is written primarily in C++, with Rust components for transpilation and encoding, TypeScript/JavaScript for Node.js compatibility polyfills, and Python support via Pyodide. Configuration uses Cap'n Proto schemas, and the build system is Bazel.
Tip: workerd is not a hardened sandbox on its own. When running untrusted code, Cloudflare wraps it in additional layers (VMs, process isolation). If you're self-hosting, you need your own sandboxing.
Top-Level Directory Map
The repository follows a clear separation between runtime source, language polyfills, build infrastructure, and distribution packaging. Here's the complete layout:
| Directory | Language | Purpose |
|---|---|---|
src/workerd/ |
C++ | Core runtime — server, I/O layer, JSG bindings, API implementations |
src/workerd/api/ |
C++ | Web API implementations (fetch, streams, crypto, cache, KV, R2, etc.) |
src/workerd/io/ |
C++ | I/O context, worker lifecycle, actor storage, compatibility dates |
src/workerd/jsg/ |
C++ | JavaScript Glue — the C++ ↔ V8 binding layer |
src/workerd/server/ |
C++ | CLI entry point, server orchestration, config schema |
src/workerd/util/ |
C++ | Shared utilities (SQLite wrapper, HTTP helpers, UUID, etc.) |
src/node/ |
JS/TS | Node.js compatibility polyfills (node:buffer, node:crypto, etc.) |
src/pyodide/ |
JS/TS | Pyodide integration layer for Python workers |
src/cloudflare/ |
TS | Cloudflare-specific module stubs |
src/rust/ |
Rust | Transpiler, encoding, Python parser, JSG Rust bindings, KJ interop |
samples/ |
Mixed | Example configurations and worker scripts |
types/ |
TS | TypeScript type definitions for the Workers API |
npm/ |
JS | npm package builds (workerd, workers-types, platform binaries) |
build/ |
Starlark | Bazel build rules and dependency management |
deps/ |
Various | Dependency configuration |
patches/ |
Diff | Patches applied to third-party dependencies (SQLite, V8) |
graph TD
Root["workerd/"] --> SrcWorkerd["src/workerd/ (C++ core)"]
Root --> SrcNode["src/node/ (JS polyfills)"]
Root --> SrcPyodide["src/pyodide/ (Python)"]
Root --> SrcRust["src/rust/ (Rust components)"]
Root --> Samples["samples/"]
Root --> NPM["npm/ (distribution)"]
Root --> Build["build/ + deps/ + patches/"]
SrcWorkerd --> Server["server/ (CLI + orchestration)"]
SrcWorkerd --> IO["io/ (context, worker lifecycle)"]
SrcWorkerd --> JSG["jsg/ (V8 binding layer)"]
SrcWorkerd --> API["api/ (Web APIs)"]
SrcWorkerd --> Util["util/ (shared helpers)"]
The most important directories for understanding the runtime are src/workerd/server/, src/workerd/io/, src/workerd/jsg/, and src/workerd/api/. Together, these four directories contain the vast majority of the runtime logic.
The Layered Architecture
workerd is organized into six conceptual layers, each with a clear responsibility. Understanding these layers is the single most important step toward navigating the codebase.
graph TB
subgraph "Process Level"
CLI["CliMain (workerd.c++)"]
Server["Server (server.c++)"]
end
subgraph "Service Level"
Service["Service (WorkerService, NetworkService, etc.)"]
end
subgraph "V8 Lifecycle"
Isolate["Worker::Isolate"]
Script["Worker::Script"]
Worker["Worker (instance)"]
end
subgraph "Request Level"
IoCtx["IoContext"]
Entrypoint["WorkerEntrypoint"]
end
subgraph "Binding Level"
JSGLayer["JSG (C++ ↔ JS bridge)"]
V8["V8 Engine"]
end
CLI --> Server
Server --> Service
Service --> Worker
Worker --> Isolate
Worker --> Script
Service --> Entrypoint
Entrypoint --> IoCtx
IoCtx --> JSGLayer
JSGLayer --> V8
Layer 1 — CLI (workerd.c++): The process entry point. Parses command-line arguments, reads configuration, and hands control to the Server.
Layer 2 — Server (server.c++, server.h): The central orchestrator. Owns all services and sockets, bootstraps V8, and runs the KJ event loop. The Server class is where everything comes together.
Layer 3 — Services (server.c++ internals): Each entry in the config's services list becomes a Service object. Services come in several flavors — WorkerService, NetworkService, ExternalHttpService, DiskDirectoryService — and can reference each other through bindings.
Layer 4 — Worker / Isolate / Script (worker.h): The V8 lifecycle hierarchy. Worker::Isolate owns a V8 isolate. Worker::Script is a compiled script within that isolate. Worker is a fully instantiated instance with bindings and a JavaScript context.
Layer 5 — IoContext (io-context.h): The per-request (or per-actor) bridge between V8's garbage-collected heap and KJ's I/O event loop. This layer manages thread safety, I/O ownership, and the request lifecycle including waitUntil().
Layer 6 — JSG (jsg/): The JavaScript Glue system. A macro-driven binding layer that lets C++ classes declare their JavaScript-visible interface without writing raw V8 API calls. Every Web API in workerd is exposed through JSG.
Three Design Principles
Three principles run deep through every architectural decision in workerd. Understanding them explains why the code is structured the way it is.
Nanoservices
Unlike traditional microservices that run in separate processes, workerd's "nanoservices" are workers that run in the same process and thread. When one worker calls another via a service binding, the call is essentially a function call — no network hop, no serialization overhead. The config file defines these service-to-service relationships declaratively.
Capability Bindings
Workers have no ambient authority. A worker cannot access the network, the filesystem, or even another worker unless it is explicitly granted a binding in the configuration. This is a capability-based security model as described in the config schema's comments in workerd.capnp:
We generally like to avoid giving a Worker the ability to access external resources by name, since this makes it hard to see and restrict what each Worker can access.
The binding types are extensive: text, data, json, wasmModule, cryptoKey, service, durableObjectNamespace, kvNamespace, r2Bucket, queue, fromEnvironment, unsafeEval, memoryCache, and more — all defined in the Binding struct.
Compatibility Dates
workerd makes a remarkable promise: updating to a newer version will never break your JavaScript code. This is enforced through compatibility dates. Each worker declares a compatibilityDate in its config (e.g., "2023-02-28"), and workerd emulates the behavior that existed on that date.
New behaviors are introduced with a date annotation. For example, in compatibility-date.capnp:
formDataParserSupportsFiles @0 :Bool
$compatEnableFlag("formdata_parser_supports_files")
$compatEnableDate("2021-11-03")
$compatDisableFlag("formdata_parser_converts_files_to_strings");
This flag fixes a bug where FormData converted files to strings. Workers with a compatibility date after 2021-11-03 get the fix automatically. Workers before that date keep the old behavior. Workers can also explicitly opt in or out using the enable/disable flag names.
A Minimal Working Example
Let's ground this in something concrete. The samples/helloworld_esm/ directory contains a complete, runnable example.
The config file defines two things — a service and a socket:
const helloWorldExample :Workerd.Config = (
services = [ (name = "main", worker = .helloWorld) ],
sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ]
);
The worker definition specifies the module and compatibility date:
const helloWorld :Workerd.Worker = (
modules = [
(name = "worker", esModule = embed "worker.js")
],
compatibilityDate = "2023-02-28",
);
And the JavaScript is as minimal as it gets — worker.js:
export default {
async fetch(req, env) {
return new Response("Hello World\n");
}
};
flowchart LR
Client["HTTP Client"] -->|"GET /"| Socket["Socket *:8080"]
Socket -->|routes to| Service["Service 'main'"]
Service -->|executes| Worker["worker.js fetch()"]
Worker -->|returns| Response["Response('Hello World')"]
To run it: workerd serve samples/helloworld_esm/config.capnp. That single command parses the Cap'n Proto config, starts V8, compiles the ES module, binds the socket, and begins serving requests.
Tip: The
embeddirective in Cap'n Proto is doing heavy lifting here — it reads the file contents at config parse time and embeds them directly into the config structure. This is how workerd avoids needing a separate file-resolution step.
Key Files to Bookmark
As you explore the codebase in subsequent articles, these are the files you'll return to most often:
| File | Role |
|---|---|
src/workerd/server/workerd.c++ |
CLI entry point |
src/workerd/server/server.h |
Server class — the orchestrator |
src/workerd/server/server.c++ |
~6000-line implementation of all services |
src/workerd/server/workerd.capnp |
Complete configuration schema |
src/workerd/io/worker.h |
Worker, Script, and Isolate lifecycle |
src/workerd/io/io-context.h |
Per-request I/O context |
src/workerd/jsg/jsg.h |
JSG macro definitions |
src/workerd/api/global-scope.h |
The JavaScript global object |
src/workerd/io/compatibility-date.capnp |
All compatibility flags |
What's Next
Now that you have the map, Part 2 will trace the complete journey of a single HTTP request — from the moment you type workerd serve through config parsing, server boot, socket binding, isolate locking, JavaScript execution, and response delivery. You'll see exactly how these layers collaborate to turn a Cap'n Proto config and a JavaScript file into a running server.