Stateful Streams
Stateful encoding is Twilic's highest-leverage optimization for long-lived connections where most fields stay stable between updates. This guide covers when to use it, how sessions work, and production recovery patterns.
When stateful wins
| Signal | Stateful fit |
|---|---|
| Connection stays open > 10 seconds | Strong |
| < 30% of fields change per message | Strong |
| Thousands of concurrent connections | Strong (egress savings compound) |
| Independent unrelated events | Poor — use Batch instead |
| HTTP request/response | Never |
Architecture
[Producer] [Consumer]
SessionEncoder SessionDecoder (or decode full frames)
| |
|-- encode() full frame ------------>| establish baseline
|-- encodePatch() 30 bytes --------->| apply patch
|-- encodePatch() 45 bytes --------->| apply patch
| (disconnect) |
|-- reset() |
|-- encode() full frame ------------>| re-establishJavaScript implementation
import { init, createSessionEncoder } from "@twilic/core";
await init();
const enc = createSessionEncoder({
enableStatePatch: true,
unknownReferencePolicy: "statelessRetry",
});
// Server WebSocket handler
ws.on("connection", (socket) => {
const interval = setInterval(() => {
const metrics = collectMetrics();
const bytes = tick === 0 ? enc.encode(metrics) : enc.encodePatch(metrics);
socket.send(bytes);
tick++;
}, 50);
socket.on("close", () => {
clearInterval(interval);
enc.reset();
tick = 0;
});
});For all encoding variants see AdvancedSessionEncoder.
Session options
Configure via SessionOptions:
| Option | Recommendation |
|---|---|
enableStatePatch | true for dashboards; false if every message is unrelated |
enableTrainedDictionary | true when string fields repeat across ticks |
enableTemplateBatch | true for columnar streaming batches |
unknownReferencePolicy | "statelessRetry" on clients that reconnect |
maxBaseSnapshots | 8 default; increase if many concurrent entity bases |
Patch selection behavior
The encoder compares the new value against the last encoded value and emits a StatePatch when fewer fields changed than a full re-encode would cost. Static fields (region, host, version) stay as Keep operations on the wire.
On a 15-field metric object with 2 changing fields:
| Method | Typical size |
|---|---|
encode() full | ~250 bytes |
encodePatch() | ~30–50 bytes |
Recovery after disconnect
Clients lose session state on reconnect. Always:
- Call
enc.reset()on the producer - Send a full
encode()frame as the first post-reconnect message - Resume
encodePatch()on subsequent ticks
function onReconnect() {
enc.reset();
send(enc.encode(currentState));
}Error handling
Count consecutive decode failures on the consumer. After a threshold, request or emit a full frame:
let errors = 0;
function onMessage(bytes: Uint8Array) {
try {
applyPatch(bytes);
errors = 0;
} catch {
errors++;
if (errors >= 3) requestFullSnapshot();
}
}See Cookbook — Graceful Degradation.
Micro-batches in streams
For streams that send small groups of related records per tick:
enc.encodeMicroBatch([entity1, entity2, entity3]);Useful when each tick carries a few same-shape objects rather than one large map.
Framing
WebSocket messages map 1:1 to Twilic frames. For TCP or message queues, prepend a length prefix:
[u32 length][twilic bytes]See Transport & Framing.
Runnable example
git clone https://github.com/twilic/examples.git
cd examples
pnpm install
pnpm example:websocket:simulate # size comparison
pnpm example:websocket # live server + client