Docs / Serverless Objects / Engine and benchmarks

Native engine and benchmarks

The in-progress native engine: what it supports, bucket backups, durable mode, threads, several nodes, running it as a service, and benchmark results.

The default engine is celld 0.6.2. The native engine is in progress: sobjects dev --engine native runs a Wrangler project's Worker and Durable Objects inside the Node process, with no celld. Each object's storage is its own SQLite file (node:sqlite) under --persist-to, which defaults to <project>/.sobjects/native.

sobjects dev ./my-worker --engine native --port 8787

The native engine is for trusted code. There's no isolate between the app and the engine, and objects get full Node APIs. Use celld for apps you don't control. It needs Node.js 22.16 or newer.

The app's own code is bundled with esbuild, which handles TypeScript and define. npm packages load from node_modules as in any Node program, so packages with native addons (for example better-sqlite3) work. cloudflare:workers and cloudflare:email resolve to the engine's shims.

What works

Area Status
Worker fetch handler, object or WorkerEntrypoint default export Yes
Durable Object IDs Yes: idFromName, newUniqueId, idFromString (forged or foreign IDs are rejected), getByName, id.name
Stubs Yes: fetch() and RPC calls with structured-clone arguments, results and errors
Input gates Yes, including blockConcurrencyWhile and async transaction()
Storage Yes: async and synchronous (ctx.storage.kv) KV, ordered list, sql.exec cursors, transactionSync, transaction, deleteAll (also clears the alarm)
Alarms Yes: delivered after a restart, retried with backoff (up to 6 retries) when the handler throws
vars, cloudflare:workers, cloudflare:email (EmailMessage only) Yes
WebSockets and the hibernation API Yes. An evicted object's hibernatable sockets stay open, and the next message wakes a new instance.
Agents SDK Yes: the conformance Agents suite passes
KV, D1, R2, Queues, Workflows, service bindings, assets Not yet. The engine refuses a config that declares them.
Idle eviction Yes: --idle-evict-seconds (default 60; 0 never)
ctx.waitUntil Yes: keeps the object in memory until the promise settles
Portability check Yes: sobjects build [--strict]

Check portability

sobjects build ./my-worker --strict

build lists each Node.js built-in the app imports, and whether Cloudflare supports it, stubs it, or needs a nodejs_compat flag or compatibility date. It also flags packages with compiled native addons. A portable app runs on Cloudflare, celld and the native engine. A native-only app runs only here. --strict exits 1 for native-only apps.

Bucket backups

export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
export S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com   # omit for AWS S3; set AWS_REGION there
sobjects dev ./my-worker --engine native --backup-bucket s3://my-bucket/node-a

With --backup-bucket, the engine copies each object's SQLite file to <prefix>/native/<script>/<class>/<id>.sqlite within --backup-interval-seconds (default 10) of a change, and again when the object closes or the engine stops. On start, objects in the bucket that have no local file are downloaded and their pending alarms scheduled. Tested against Cloudflare R2 and SeaweedFS.

Limits of interval mode:

  • The local file is the source of truth. If the disk is lost, writes since the last upload are lost too.
  • Each upload sends the whole file.
  • One node per prefix. A node that finds a copy another node wrote stops backing that object up and logs it.
  • There is no point-in-time restore in this mode; enable bucket versioning if you need history.

Durable mode

sobjects dev ./my-worker --engine native --backup-bucket s3://my-bucket/node-a --backup-mode durable

In durable mode, nothing the engine acknowledges is lost if the disk is:

Mechanism Behavior
Output gate After each event, the engine reads the transactions the object committed from its SQLite WAL and holds the event's result until they are in the bucket.
Batching Transactions from every object on a thread go up together, one gzipped segment per flush.
Power loss Object databases run with synchronous = FULL.
Snapshots Each object gets a full copy every 2,000 transactions or 16 MB of log. Compaction runs every 10 minutes.
Point-in-time recovery Back --backup-retention-days (default 7) with Cloudflare's API.
Bucket outage After 30 seconds, waiting events fail instead of being acknowledged. Their writes ship once the bucket answers.

Limits:

  • A write costs one batched bucket round trip: a few ms to a local MinIO, typically 10 to 40 ms to S3 or R2 in the same region.
  • Fetches and WebSocket messages an object sends while its writes are pending aren't held back.
  • Writes from timers or waitUntil outside an event ship within 200 ms, with no gate.
  • A node in durable mode doesn't start until it can reach its bucket.
  • The store needs conditional writes (If-None-Match). AWS S3, Cloudflare R2, SeaweedFS and MinIO from RELEASE.2025-09-07 support them. Backblaze B2, Garage and DigitalOcean Spaces don't. The node checks this at start.

Point-in-time recovery

const bookmark = await this.ctx.storage.getBookmarkForTime(Date.now() - 60 * 60 * 1000);
const undo = await this.ctx.storage.onNextSessionRestoreBookmark(bookmark);
this.ctx.abort(); // the next request sees the object as it was an hour ago

getCurrentBookmark() and getBookmarkForTime(time) return bookmarks. onNextSessionRestoreBookmark(bookmark) restores on the object's next start and returns a bookmark that undoes the restore. A bookmark the bucket can no longer rebuild, or a time before the kept history, is refused with a RangeError. Without durable mode, these methods throw.

Worker threads

sobjects dev ./my-worker --engine native --threads 4

Each Durable Object lives on exactly one thread, chosen from its ID. A thread that doesn't answer for --thread-stall-seconds (default 30), throws an uncaught error, or exceeds --thread-memory-mb is replaced, and its objects reload from their SQLite files.

  • Threads are not a security boundary.
  • A call that crosses threads costs roughly 0.3 to 0.5 ms more.
  • In one test, 16 CPU-bound objects went from 89 req/s on one thread to 169 req/s on 4 and 182 on 8 (M1 Pro).

Several nodes

# on each node, with a unique, stable name and the same secret
export SOBJECTS_CLUSTER_SECRET=...   # 32+ characters, identical on every node
sobjects dev ./my-worker --engine native --backup-bucket s3://my-bucket/app --backup-mode durable \
  --node-name node-a --peer-listen 10.0.0.5:9790

With --node-name, nodes in durable mode share one bucket prefix and each object runs on one node at a time.

Topic Behavior
Leases Each node renews its lease every third of --lease-seconds (default 10). A node that can't renew stops acknowledging writes and exits.
Takeover When an object's owner released it or its lease ran out, another node claims it and rebuilds it from the bucket.
Alarms Every node checks for due alarms whose owner is gone and claims those objects.
Draining On SIGINT or SIGTERM a node drains for up to --drain-seconds (default 30), closes client WebSockets with 1001 and releases its objects.
Forwarding With --peer-listen, a call for an object another node runs is forwarded to that node.
Peer security Peer requests are signed with SOBJECTS_CLUSTER_SECRET (HMAC-SHA256) but not encrypted. Bind the peer listener to a private network, never the public internet or the ingress.

Not yet: forwarding without --peer-listen, TLS between nodes, --node-name combined with --threads, and cross-node compaction.

Running as a service

sobjects up runs the native engine when sobjects.toml has a [native] table in place of [celld]:

[native]
project = "./my-worker"          # the Wrangler project to run; relative to this file
listen = "127.0.0.1:8787"        # default
# data_dir = "/var/lib/serverless-objects/native"  # default: $SOBJECTS_DATA_DIR/native
# idle_evict_seconds = 60
# drain_seconds = 30
# threads = 2

[native.backup]
bucket = "s3://my-bucket/app"
mode = "durable"                 # default here; "interval" is the other choice
# retention_days = 7
# endpoint = "https://<account>.r2.cloudflarestorage.com"
# region = "auto"

# [native.cluster]
# node_name = "node-a"
# lease_seconds = 10
# peer_listen = "10.0.0.5:9790"
# peer_advertise = "http://10.0.0.5:9790"

Credentials and SOBJECTS_CLUSTER_SECRET come from the environment, never the file. For a systemd unit, put them in sobjects.env beside the config.

  • --detach, status, logs and down work as with celld. down waits up to a minute for the engine to upload what's left.
  • sobjects service install writes a unit that restarts the node if it exits, including after it loses its lease.
  • [ingress] routes to [native].listen by default.
  • [admin] and the dashboard aren't available with [native] yet. sobjects deploy is for celld: restart the node to run new code.

Without [native.backup], objects exist only in data_dir. The full key list is in the CLI reference.

Benchmarks

packages/conformance/bench-app is one Worker with one Durable Object class. The same code runs on Cloudflare, Wrangler, celld and the native engine.

npm run bench --workspace=sobjects-conformance -- --target native     # or celld, wrangler
npm run bench --workspace=sobjects-conformance -- --url https://sobjects-bench.<you>.workers.dev

Durability differs by target

Writes are not equally durable across targets, and that dominates the write rows:

Target When a write is acknowledged
Cloudflare After storage replicated across machines confirms it (the output gate).
celld After the node has proven the write durable. A single node needs bucket proof.
Native engine, default After a local SQLite WAL commit with synchronous = NORMAL. Survives a process crash, not power or disk loss.
Wrangler wrangler dev --local, a development server with local SQLite.

Results, 20 Oct 2026

Client: Apple M1 Pro, Node.js 24.12. Local targets on loopback: serverless-objects 0.5.0 native engine, celld 0.6.2 (celld dev), Wrangler 4.150.0. Cloudflare: the same app on a workers.dev subdomain on the free plan, served from Frankfurt, about 130 to 200 ms from the client. One run each; treat differences under about 20% as noise.

Inside the Worker, mean per call (500 sequential calls to one object, 100 for SQL):

Mean per call, ms Cloudflare native celld Wrangler
DO read (RPC + KV get) 10.89 0.03 0.07 0.39
DO write (RPC + KV put) 19.32 0.05 9.29 0.68
100-row SQL transaction 39.08 0.35 9.90 4.86

From the client:

p50 / p99 ms Cloudflare native celld Wrangler
noop (Worker only) 202 / 571 0.28 / 1.11 0.32 / 3.39 1.32 / 2.72
DO read 222 / 482 0.25 / 0.91 0.41 / 1.11 1.60 / 7.27
DO write 301 / 744 0.22 / 1.09 8.70 / 24.39 1.68 / 3.56
100-row SQL transaction 253 / 740 0.51 / 1.20 9.48 / 21.30 4.69 / 21.94
WebSocket echo (hibernatable) 151 / 373 0.08 / 0.17 0.13 / 0.19 0.30 / 0.89
Writes, 32 in flight, req/s (p99 ms) Cloudflare native celld Wrangler
1 object 136 (871)* 4,507 (44.0) 1,705 (39.5) 1,437 (48.4)
32 objects 153 (623)* 9,721 (9.9) 178 (2,780) 1,207 (92.4)

* Limited by the client: 32 requests in flight over a roughly 200 ms round trip caps out near 160 req/s. These rows say nothing about Cloudflare's capacity.

Native engine in durable mode, against a local MinIO, 32 requests in flight spread over N objects:

Objects 1 2 4 8 16 32
writes/s 1,199 1,442 1,367 1,593 1,376 1,207
p50 ms 24.8 21.5 21.9 18.8 22.3 21.7

Reading the results

  • On one machine, the native engine and celld have no network hop to the object, which is where most of their advantage over Cloudflare comes from. It is the trade-off of running on one node, not a faster implementation of the same guarantees.
  • celld's single-node write throughput falls as more objects write at once (from about 1,800 to 240 writes/s under celld serve with SeaweedFS).
  • The native engine in its default mode is the fastest target and its writes are the least durable. Choose it when the app is trusted and a few seconds of writes on one machine are an acceptable loss. Choose Cloudflare or celld when every acknowledged write must survive the machine. Durable mode closes this gap for the native engine at the cost shown in its table.

Full tables and methodology: benchmarks source and native engine source.