Cells/Limitations

Cell Limitations

These are the current boundaries of what Cells can do. Most reflect deliberate design choices; some are areas of active development.


Storage and state

No persistent local filesystem. Cells are stateless. There is no writable disk — only the bindings declared in ribo.toml provide persistent storage. Use c3 for structured data and g7 for files and binary objects.

No shared memory. Cells are isolated from each other. Two Cells cannot share a variable, a cache, or any in-memory state. Communication must go through c3 or g7.


Networking

fetch() only. Cells can make outbound HTTP/HTTPS requests using the standard fetch() API. There is no access to raw TCP/UDP sockets, DNS resolution, or other network primitives.

Outbound requests go through an egress gateway. Every request your Cell's fetch() makes is routed through a gateway that runs outside your Cell, and a request to an internal address is refused before it reaches a socket. Refused requests come back as HTTP 403 with a JSON body:

{ "error": "egress_blocked", "message": "fetch to internal address blocked: http://10.0.0.5/" }

What is refused:

Examples
Loopback and private ranges 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 (incl. cloud metadata 169.254.169.254), 100.64.0.0/10
IPv6 equivalents ::1, fc00::/7 (ULA), fe80::/10 (link-local), IPv4-mapped forms of the above
Internal hostnames localhost, metadata.google.internal, metadata, instance-data
Platform infrastructure The tissue servers' own addresses
Non-HTTP schemes Anything that is not http: or https:

Obfuscated forms of a blocked address are decoded before the check — http://2130706433/, http://0x7f.1/ and http://127.1/ are all 127.0.0.1, and all refused.

This has no effect on ordinary use: calling a public API, a webhook, or another Cell by its *.tissue.dev (or custom-domain) name all work normally. Cells reach c3, g7 and other platform services through their bindings, not through fetch(), so those are unaffected too. If you need a Cell to talk to a service on a private network, that is not currently supported — the gateway has no allowlist you can extend yet.

No WebSockets. Neither inbound nor outbound WebSocket connections are currently supported. Long-lived connections must be modelled as a sequence of HTTP requests.


Compute

ctx.waitUntil() is available. A Cell's fetch(request, env, ctx) receives a standard ExecutionContext. Call ctx.waitUntil(promise) to keep the Cell running after the Response is returned to the client, until promise settles — useful for flushing logs or writing an analytics event. For background work that isn't tied to a specific request, use pulse to run a handler on a cron schedule instead.

No spawning processes or threads. Cells run in a single-threaded V8 context. You cannot spawn child processes or worker threads.


Crypto

PBKDF2 is capped at 1,000,000 iterations. crypto.subtle.deriveBits() with a PBKDF2 algorithm refuses an iteration count above one million:

NotSupportedError: Pbkdf2 failed: iteration counts above 1000000 are not supported (requested 2000000).

The ceiling exists because a Cell shares a machine with other tenants and an unbounded key-derivation loop is a way to monopolise it. It sits well above current guidance — OWASP recommends 600,000 iterations for PBKDF2-HMAC-SHA256 — so password hashing at a recommended work factor is unaffected. If you need a higher work factor than the cap allows, use a memory-hard KDF (scrypt or Argon2, via WASM) rather than raising the iteration count.

The rest of the Web Crypto API is standard: crypto.subtle digest/sign/verify/encrypt/decrypt, crypto.getRandomValues(), and crypto.randomUUID() all behave as specified, with no tissue-specific limits.


WASM Cells specifically

JSON serialization boundary. Request and response data crosses the JS/WASM boundary as JSON strings. This means binary request bodies are not supported for WASM Cells, and binary response bodies must be base64-encoded or represented as text.

No streaming. Because the whole request must be serialized to JSON before fetch is called, and the whole response deserialized from JSON after it returns, request and response bodies are fully buffered for WASM Cells. JS Cells don't have this restriction — request.body and Response bodies are standard ReadableStreams and are streamed in both directions. See js-stream-demo.

No async Rust without wasm-bindgen. Standard Rust async runtimes (Tokio, async-std) do not work in WASM. Async from WASM requires wasm-bindgen futures, which adds complexity.


Code size

A deploy upload (JavaScript source, or WASM binary + glue) is capped at 20 MB. Below that there is no per-file limit, but large bundles increase cold-start latency — keep compiled WASM under a few megabytes for best performance.


See also