Skip to content

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

SignalStateful fit
Connection stays open > 10 secondsStrong
< 30% of fields change per messageStrong
Thousands of concurrent connectionsStrong (egress savings compound)
Independent unrelated eventsPoor — use Batch instead
HTTP request/responseNever

Architecture

text
[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-establish

JavaScript implementation

ts
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:

OptionRecommendation
enableStatePatchtrue for dashboards; false if every message is unrelated
enableTrainedDictionarytrue when string fields repeat across ticks
enableTemplateBatchtrue for columnar streaming batches
unknownReferencePolicy"statelessRetry" on clients that reconnect
maxBaseSnapshots8 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:

MethodTypical size
encode() full~250 bytes
encodePatch()~30–50 bytes

Recovery after disconnect

Clients lose session state on reconnect. Always:

  1. Call enc.reset() on the producer
  2. Send a full encode() frame as the first post-reconnect message
  3. Resume encodePatch() on subsequent ticks
ts
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:

ts
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:

ts
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:

text
[u32 length][twilic bytes]

See Transport & Framing.

Runnable example

bash
git clone https://github.com/twilic/examples.git
cd examples
pnpm install
pnpm example:websocket:simulate # size comparison
pnpm example:websocket # live server + client

Released under the CC-BY-4.0 License.