Native lease context is part of the 2026-09-30 Statewire wire contract.
StatewireWebsocket carries frames and packets over WebSocket. StatewireHttp uses HTTP requests and an SSE stream.
Connections
An attach is one live subscription to an instance. It begins with a hello packet and ends when its socket or stream closes.
A client record holds durable-lane command history for a client id. It can serve several concurrent attaches. Ephemeral lanes, their documents, and every protocol's context belong to each attach.
- Clients generate 32 lowercase hexadecimal characters. Servers accept
[A-Za-z0-9._~-]{1,256}. - WebSocket carries the id in the
clientquery parameter. HTTP stream requests carry it inStatewire-Client-Id. - Each attach receives its own snapshot. Attaches sharing a client id share durable-lane sequence numbers and command answers.
StatewireWebsocket
Open {url}/ws?client=<id> with subprotocol statewire.v1.
The first text message supplies transport headers:
{
"headers": {
"Statewire-Version": "\"2026-09-30\"",
"Statewire-Protocol": "default; version=\"2026-09-30\""
}
}- Additional offered subprotocols follow
statewire.v1. - The initial message may also include
ctxreplacements andcmdstatements (attach statements). - Send the initial message within 5 seconds, then wait for the hello before sending frames. The timeout is
wsInitTimeoutMs/ws_init_timeout. - Send context replacements in socket order; hosts serialize their installation.
- Send each later frame as one text message on the same socket. Each server text message contains one packet.
- Command answers and admission refusals return over that socket; durable-lane answers also reach other attaches sharing the client id.
StatewireHttp
StatewireHttp receives packets through an SSE stream and submits later frames through separate HTTP requests.
A lease identifies one live attach. The stream response supplies it in Statewire-Lease; later frame requests present it in the same header.
GET /stream
Send GET /stream to receive state updates and command results:
GET /stream
Host: example.com
Statewire-Client-Id: 0123456789abcdef0123456789abcdef
Statewire-Version: "2026-09-30"
Statewire-Protocol: default; version="2026-09-30"The response is an SSE stream (Content-Type: text/event-stream), starting with the hello packet.
200 OK
Content-Type: text/event-stream
Statewire-Lease: example-lease
data: {"syn":{"version":"2026-09-30","protocols":[{"name":"default","version":"2026-09-30","seq":0}]},"ops":[{"op":"mount","protocol":"default","main":true,"value":{"count":0}}]}
POST /stream
Opens the stream and immediately sends a frame in the request body:
POST /stream
Host: example.com
Statewire-Client-Id: 0123456789abcdef0123456789abcdef
Statewire-Version: "2026-09-30"
Statewire-Protocol: default; version="2026-09-30"
Content-Type: application/json
{"cmd": [{"seq": 1, "protocol": "default", "method": "increment", "params": [1]}]}200 OK
Content-Type: text/event-stream
Statewire-Lease: example-lease
data: {"syn":{"version":"2026-09-30","protocols":[{"name":"default","version":"2026-09-30","seq":1}]},"ops":[{"op":"mount","protocol":"default","main":true,"value":{"count":0}}]}
data: {"ops":[{"op":"replace","path":["count"],"value":1}],"cmd":[{"protocol":"default","seq":1,"type":"applied"},{"protocol":"default","seq":1,"type":"result","payload":1}]}
Submit later frames through POST /frames.
Stream packets and leases
Each SSE data: event contains one packet, followed by a blank line:
data: {"idle":true}
- Each concurrent attach has its own lease. Its authorization expires after the host's maximum lease age (default 24 hours); heartbeats and frame submissions preserve that deadline. Expiry sends
fin: {reason: "reconnect", code: "lease-expired"}; a hibernating host (Cloudflare Durable Objects) sends it to a WebSocket attach on its next frame, its next packet, or when the host restores the socket. A frame using an expired lease receives423 stale-lease; a new attach authorizes again. Hosts may disable expiry withmaxLeaseLifetimeMs: nullormax_lease_lifetime = None. - A response containing
Statewire-Leasesupports writes throughPOST /frames. If the header is omitted, submit statements in anotherPOST /stream, opening another attach. - Cross-origin hosts expose the lease to browsers:
Access-Control-Expose-Headers: Statewire-Lease. A browser treats an unexposed header as omitted.
POST /frames
Submits a frame for the attach identified by Statewire-Lease. The server resolves the lease to the client record, the attach's protocol contexts, ephemeral lanes and document view, and its authorization result.
POST /frames
Host: example.com
Statewire-Lease: example-lease
Content-Type: application/json
{"cmd": [{"seq": 1, "protocol": "default", "method": "increment", "params": [1]}]}200 OK
Content-Type: application/json
{}Use the lease returned by the stream in place of example-lease. Command answers arrive on the stream.
| Status | Body | Meaning and next action |
|---|---|---|
200 | {} | Context installed and commands admitted. Mark statements posted and await their stream answers. Context-only frames require no command answer. |
409 | {"syn": {"protocols": [{"name": ..., "seq": ...}]}} | Context installed; new commands refused for a sequence gap. Reconcile with the per-lane watermarks and resend commands. |
429 | {"syn": {"protocols": [{"name": ..., "seq": ...}], "retry": ...}} | Host admission pushback leaves context installed and new commands unadmitted. Wait, then resend commands. A lower-layer 429 does not confirm context installation. |
423 | {"error": "stale-lease"} | Lease is stale. Reattach with the latest retained context, then resend pending durable-lane commands under their original sequence numbers, omitting the stale frame's ctx. Unanswered ephemeral-lane commands are lost; newly queued ephemeral commands start at 1. |
400 | {"error": "<reason>"} | Protocol violation. The stream also ends with fin: protocol-error. |
413 | {"error": "frame body too large"} | Body exceeds 8 MiB of UTF-8. Split the unadmitted frame. |
- A valid lease supplies the attach's captured authorization result for the submitted commands.
- On
429,syn.retryis the delay in milliseconds.Retry-Afterrounds it up to whole seconds. Clients use a parseable delta-secondsRetry-Afterwhen a lower layer returns429without a usable refusal body. - Allow one POST in flight per lane. A frame spanning several lanes occupies each of them until its POST completes.
- Context-bearing POSTs occupy the attach's context write channel until they complete. Serialize context writes with command frames whose admission reads that context.
- After an uncertain outcome of a context-bearing POST, retire its lease, attach with the latest retained context, and resend pending durable commands under their original sequence numbers. Hosts fence late requests and context-change work to their originating attach.
- After a timeout of a command-only POST, resend the original statements under their sequence numbers. The retry may append more consecutive statements. Admission and recovery handle repeated submissions.
Authorization
Authorization runs once per attach and binds its result to that connection. Credential changes require a new attach.
- StatewireHttp authorizes
/streamusing HTTP request headers./framesuses the authorization result associated with its lease. Aheadersfield in any HTTP frame is a protocol violation. - StatewireWebsocket carries the required negotiation headers in its initial message. These headers fill names absent from the WebSocket handshake headers. A handshake header wins a collision.
- Header names and values in the initial message must be valid. Duplicate names are rejected before authorization.
- Later StatewireWebsocket frames containing
headersare a protocol violation.
| Attach problem | HTTP | WebSocket |
|---|---|---|
Missing initial-message headers | — | fin: unauthorized, status 401, then close 1008 |
Malformed initial-message headers | — | fin: protocol-error, message malformed headers, then 1008 |
| First-frame timeout | — | fin: unauthorized, then 1008 |
| Credentials missing, invalid, or expired | 401 | fin: unauthorized, then 1008 |
| Authorization denies access | 403 | fin: forbidden, then 1008 |
| Different authorization result for an existing client id | 403 | fin: forbidden, then 1008 |
Concurrent attaches and record lifetime
The first attach binds the client id to its authorization result. Later attaches must match it until the client record is evicted.
- Ephemeral lanes and their documents end with their attach; each attach restates them from
seq1. - Idle records are evicted least-recently-used. Live records and records holding commands are exempt.
- Protocol context ends with its attach; clients restate the latest values on each new attach, including preserved-socket wake. Already admitted commands retain their captured context.
- Durable-lane admission survives host restarts. Commands replayed after a restart run without an attach.
| Limit | Setting | Default |
|---|---|---|
| Concurrent subscribers | maxSubscribers / max_subscribers | 1024 |
| Client records | clientRecordLimit / client_record_limit | 1024 |
An attach exceeding the subscriber cap receives HTTP 503, or WebSocket fin: reconnect with code: overloaded, followed by 1013.
Version negotiation
- Dated attaches require
Statewire-VersionandStatewire-Protocol. They use inclusive date ranges. - WebSocket requires the fixed bootstrap subprotocol
statewire.v1. - HTTP and WebSocket return selected versions in
syn.versionandsyn.protocolsbefore application state. - Invalid negotiation returns HTTP
400; unsupported ranges or required protocols return406. The JSON body carriesfin: protocol-errorwithcode: invalid-negotiation,unsupported-version, orunsupported-protocol. - WebSocket sends the same
finand closes with1008. - Each lease retains its attach's selection. Negotiation headers on
/framesleave that selection unchanged. - Initial commands are accepted with version ranges. Clients tighten their minimums when initial statements require newer features.
Heartbeats
A heartbeat keeps an otherwise quiet connection alive.
| Transport | Heartbeat |
|---|---|
| WebSocket | Empty object {} |
| SSE | Comment : ping |
- Hosts send a heartbeat after 15 seconds without outgoing bytes.
- WebSocket heartbeats flow both ways. Clients send
{}every 10 seconds once the attach frame is sent. - Hosts answer a client
{}with{}once the attach frame is received. A heartbeat is never a frame or a packet. - After 30 seconds of receive silence, clients reconnect.
- The same 30-second timeout applies while waiting for the initial hello and to each POST.
Violations
A violation is invalid client input, such as a malformed frame or a broken sequence within a lane. A gap relative to the server watermark is an admission refusal.
| Input | WebSocket | HTTP |
|---|---|---|
| Malformed frame or another frame-rule violation | fin: protocol-error, then 1008 | 400; a violating POST /frames also ends the stream |
| Text frame or body over 8 MiB of UTF-8 | fin: protocol-error, then 1009 | 413 before parsing |
| Invalid client id | 1008 | 400 |
- A missing client id on
/streamreturns400 {"detail":"missing Statewire-Client-Id header"}. An invalid id returns400 {"detail":"invalid Statewire-Client-Id: <reason>"}. - Protocol violations stop automatic reconnection.
- Client actions for
finreasons follow Connection ending.
Host handling
- Initial
ctxis validated and installed after protocol selection and authorization, before the hello and initial commands. Later WebSocket frames received during authorization are processed after the hello. - Validate WebSocket client ids before accepting the socket where the adapter supports it.
- Binary WebSocket frames receive
fin: protocol-error, messagemalformed frame, then close1008. - A pending WebSocket attach occupies a subscriber slot after its initial message.
- An outgoing queue exceeding
streamQueueSize/stream_queue_size(default 256) ends withfin: reconnect,code: client-fell-behind. - Unexpected authorization or activation failures end with
fin: errorand WebSocket close1011.