Self-hosted Workers platform

Run Cloudflare Workers on servers you own.

Nodeflare runs your Worker code unchanged on workerd, the open-source Workers runtime, and gives every binding a backend you operate: KV, R2, Queues, Durable Objects, Workflows. Deploy with the wrangler you already use.

Early stage, October 2026 Targets workerd 1.20261001.1 and Miniflare 5.20261001.0
node-pool-a.capnp generated
# workerd config, generated per node by the control plane
services = [
  (name = "tenant1-app", worker = (
    bindings = [
      (name = "KV",    kvNamespace = "kv-sidecar"),
      (name = "FILES", r2Bucket    = "r2-adapter"),
      (name = "JOBS",  queue       = "queue-broker"),
    ],
  )),
  (name = "kv-sidecar",
   external = (address = "unix:/run/nodeflare/kv.sock")),
  (name = "r2-adapter",
   external = (address = "unix:/run/nodeflare/r2.sock")),
  (name = "queue-broker",
   external = (address = "unix:/run/nodeflare/queue.sock")),
]
Every binding is a service. Every service is one we run.

How it works

A thin shim around workerd.

In workerd's config, KV, R2 and Queue bindings are service designators: workerd turns every call on a binding into an HTTP request to the named service. So "our own KV" is an HTTP adapter that speaks workerd's wire format in front of a real store, not a storage engine written from scratch. Here is one request, hop by hop.

01

DNS

GeoDNS or anycast sends the client to the nearest region's L4 balancer.

02

Gateway

Terminates TLS, maps hostname to tenant and script, applies limits, forwards to a workerd node.

03

workerd

Runs the script's fetch() in its isolate, exactly as Cloudflare's runtime would.

04

Bindings

Binding calls become HTTP requests to adapters, over Unix sockets when the adapter is node-local.

05

Response

Returns through the gateway. Logs flow from a tail worker into the log pipeline.

Bindings

Every binding, backed by something you can run.

Your code calls env.KV.get() or env.FILES.put() the same way it does on Cloudflare. Underneath, each binding maps to an adapter Nodeflare builds and a backend you choose.

What backs each Workers binding in Nodeflare
BindingWhat Nodeflare buildsBackend
KVAdapter plus node-local cache, one sidecar per nodeHTTP to a serviceOrdered KV store (partitioned Postgres to start, TiKV or FoundationDB later); object store for large values
R2Stateless, streaming adapterHTTP to a serviceAny S3-compatible store: Garage, Ceph RGW, SeaweedFS
QueuesBroker, per-node agent worker, cron schedulerHTTP for producers; queue() on a service binding for consumersNATS JetStream or Redpanda; Postgres while volume is low
Durable ObjectsProxy binding, placement service, ingress worker, WAL shippingIn-process only, so placement is oursSQLite on local NVMe, shipped continuously to the object store
WorkflowsEngine ported from Miniflare's MIT implementationworkflowsEngine actor classRides on the Durable Object pool
Cache APINode-local cache servicecacheApiOutbound serviceLocal disk
D1Thin serviceWrapped binding over workerd's D1 moduleDurable Object with SQL enabled
HyperdriveNothing to buildBinding points at a servicePgBouncer

Miniflare is the only complete open implementation of every binding protocol. Its source is the spec, and the conformance oracle every adapter is tested against on each pinned workerd version.

The hard parts

Two things are genuinely hard. They get the most attention.

Most of the platform is adapters and config generation. These two are not.

Durable Objects across nodes

The constraint: workerd keeps every object local to one process, and its only durable storage mode is marked experimental. Two writers on one SQLite file corrupt it.

  • Objects hash into 4096 virtual shards. A placement service on etcd leases each shard to exactly one node.
  • Your code gets a proxy binding: a direct in-process call when this node owns the object, an HTTP hop to the owner when it does not. idFromName is identical everywhere.
  • The gateway routes DO-affine requests straight to the owner node so the fast path hits.
  • SQLite WAL ships continuously to the object store. A dead node means a short pause for its objects, not lost state.

Tenant isolation

The constraint: V8 isolates in open-source workerd are not a hard security boundary, and the static config exposes no CPU or memory limits.

  • Untrusted tenants run in a workerd process per tier inside gVisor or Firecracker, capped with cgroups.
  • Per-worker CPU and subrequest limits come from workerd's dynamic loader. Request timeouts and concurrency caps sit at the gateway.
  • Egress goes only through a network service that allows public addresses, so workers cannot reach internal ones.
  • The workerd version is pinned fleet-wide, and conformance tests run against it on every upgrade.

Where it runs

Starts on one box. Grows to several regions.

The control plane is global and never on the request path. Everything else is regional, so each stage adds nodes without changing the shape.

1

One box

Docker Compose, trusted tenants. Enough to run real Workers with every binding.

PostgresGarageNATSetcd
2

One region

Kubernetes with node pools per role: gateway, workerd, stateful, adapters.

Partitioned Postgres or TiKVCeph or Garage clusterRedpanda
3

Several regions

GeoDNS in front, Durable Object namespaces pinned to a region, object store replicated asynchronously.

Same stackAsync object replication

Build order

What gets built, in order.

Each piece ships with conformance tests against Miniflare's reference workers before the next one starts.

  1. KV sidecar, with conformance tests against Miniflare's KV worker
  2. R2 adapter against an S3-compatible store
  3. Queue broker, agent worker and cron scheduler
  4. Control plane with wrangler-compatible upload, config generator and rollout, plus the gateway
  5. Durable Object proxy, placement service, ingress worker and WAL shipping
  6. Workflows engine, Cache API and D1