Cloudflare compatibility
What runs, what is planned, and what is out of scope. The governing rule, inherited from celld and mirrored by the platform: a gap fails loudly, at deploy time or first use, instead of silently. A silent gap is a bug.
celld's scope rule: if Cloudflare builds a function on Durable Objects, celld can get that function; a function built on a different primitive is out of scope. That is why D1 is planned (a D1 database is a Durable Object with a SQL API) while KV is not (a different consistency model).
Service matrix
| Status | Services |
|---|---|
| Supported | Module Workers (fetch, JS RPC, service bindings, vars) · Durable Objects (SQLite storage, alarms, hibernatable inbound WebSockets, outbound WebSocket clients, one writer per cell, names as addresses, RPC on stubs) · Static assets (assets.directory, binding, html_handling, not_found_handling, run_worker_first, _headers, _redirects; asset-only projects deploy without a Worker) · WebAssembly · a substantial node: subset · Worker Loader / Code Mode (experimental) |
| Planned upstream | D1 · Workflows · Queues (if demand appears). All are built on Durable Objects |
| Not planned | KV · R2 (declared r2_buckets bindings load, but every method throws) · Cache API · cron scheduled handlers · Workers AI (an experimental HTTP adapter exists behind CELLD_AI_URL) · Vectorize · Hyperdrive · Browser Rendering · Email · custom domains · TLS termination |
Runtime API status
| API | Status on celld |
|---|---|
| Fetch / Request / Response / Headers | Yes. Gaps: Response.redirect(), Response.error(), the cache request option. |
| Handlers | fetch, alarm, webSocketMessage/Close/Error, RPC. No scheduled (cron), queue, tail, or email. |
| JS RPC | Yes: WorkerEntrypoint, RpcTarget, promise pipelining, stubs in DO storage. Limit: a stub cannot cross an isolate boundary yet, and a cross-isolate service binding with a named entrypoint supports single method calls only. |
| Streams | Yes, including byte streams, BYOB, CompressionStream/DecompressionStream. Gap: ReadableStream.from(). |
| WebSockets | Yes: inbound hibernatable sockets with attachments, outbound clients, setWebSocketAutoResponse (answers without waking the cell). Gap: getTags(). |
| Web Crypto | Partial: digest, HMAC sign/verify, AES-GCM/CBC/CTR, Ed25519 and ECDSA P-256 signing, RSA-OAEP decrypt (not encrypt), and most import/export paths, plus Cloudflare's timingSafeEqual and DigestStream. Missing: wrapKey/unwrapKey, RSA-PSS signing, HKDF/PBKDF2 via deriveBits (both available through node:crypto). An unavailable algorithm throws. |
| Web standards | URL, URLPattern, AbortController/AbortSignal, Blob/File/FormData, structuredClone, queueMicrotask, encodings, atob/btoa. Caveats: a signal does not abort across an RPC call; signal.onabort is accepted but never invoked (use addEventListener('abort')); structuredClone is not conformant on exotic types. |
| WebAssembly | Yes: V8's own, without restrictions. .wasm imports yield compiled modules (same rule as Wrangler), compiled once per process. Rust via workers-rs works. |
| Timers | setTimeout, setImmediate, scheduler.wait(). setInterval throws. performance.now() has millisecond resolution. |
node: compat | Always available: celld externalizes node:* at bundle time and does not need (or read) the nodejs_compat flag. Implemented: assert, async_hooks (real AsyncLocalStorage), buffer, events, path, stream, timers/promises, util. Partial: crypto, zlib (sync gzip/deflate only), fs (reads fail with ENOENT). Not implemented: http(s), net, tls, dns, os, process (the process global exists; the module does not), vm, worker_threads, child_process. |
Context (ctx) | waitUntil, props, exports. passThroughOnException() is accepted but has no effect — there is no CDN behind it. ctx.facets is absent. |
| Console | log/info/warn/error are real. debug/trace/group/table do nothing; assert/time/count are absent. |
EventSource, MessageChannel, BroadcastChannel | No. The classes exist so bundles load, but they do nothing. |
Cache (caches), HTMLRewriter, TCP sockets, facets | No. |
Known silent gaps. celld's rule is that an unavailable API fails
loudly, and most do — an unknown Wrangler key fails the deploy, an unavailable Web Crypto
algorithm throws, setInterval throws. These are the upstream-documented
exceptions, which accept your call and quietly do nothing:
cloudflare:sockets'connect()returns an inert stub instead of throwing- unimplemented
node:modules import as inert stubs EventSource,MessageChannel,BroadcastChannelconstruct but do nothingctx.passThroughOnException()is accepted with no effectsignal.onabortis accepted but never invokedconsole.debug/trace/group/tabledo nothing- a compatibility flag celld does not model is accepted without effect (and is absent
from
Cloudflare.compatibilityFlagsrather than reported as enabled)
Wrangler configuration
celld deploy accepts wrangler.jsonc or wrangler.json
(not wrangler.toml) with a strict key allowlist:
name · main · compatibility_date · compatibility_flags · durable_objects · migrations · assets · services · vars
Any other key (routes, kv_namespaces, triggers, …)
stops the deploy with an error naming the key. Run
celld deploy . --dry-run in CI so unsupported configuration fails on the pull
request, never at runtime.
Honored compatibility flags: js_rpc, delete_all_deletes_alarm,
fetcher_no_get_put_delete, websocket_standard_binary_type, plus
assets navigation behavior. An unmodeled flag is accepted without effect and reported
absent from Cloudflare.compatibilityFlags.
Working around the gaps
Neither celld nor the operator ships emulated bindings: a missing service stays missing rather than becoming a fake that behaves differently under load. Today the gaps are closed in application code, not by the platform:
- Scheduled work: use Durable Object alarms, which are first-class in celld — a cell can reschedule itself after each run. celld declines cron triggers for exactly this reason. There is no operator-provided scheduler.
- Blob storage: call S3, R2, or GCS over
fetchwith credentials you supply throughspec.vars. A declaredr2_bucketsbinding loads but every method throws, so reach for the HTTP API instead. - D1: adopt it when it lands upstream; neither project front-runs it.
Entity, not process
Durable-execution engines (Temporal, Restate, Azure Durable Functions) model a process: a workflow that runs to completion. A cell models an entity: a user, a room, a document, an agent. It is a small stateful server that lives as long as its name does. If your domain decomposes into named entities, cells shard it by construction.