Skip to content

Releases

v3.1.0

Choose a tag to compare

@github-actions github-actions released this 09 Nov 23:28
7738add
  • A handler that keeps throwing can no longer stall a player's events or end the session. A rejected event is now
    offered at most events.maxRejections times (default 10, about a second), then dropped and acknowledged, so the
    events behind it continue in order. If the queue behind it fills first, it gets one last offer and is then dropped,
    instead of the session being closed with Event sequence gap. stats.eventsDropped counts these and onError
    reports each one. An ack can now also mean the receiver gave up on the event. This changes the previous "offered
    until accepted" behavior: a handler that throws to wait for something slower than a second should set
    maxRejections: Infinity, which keeps the old behavior, including session loss on a full queue.
  • Extra copies of a rejected event, such as its datagram mirror, wait for the 100 ms retry delay and no longer count
    as attempts. Code using ReliableEvents directly must call drain to retry a rejected event.
  • The rejected-event change above needs no wire change and is local to the receiving end.
  • A resumed welcome now carries the server's event counters (r received, a acknowledged). These fields are
    additive and wire generation 5 is unchanged: older clients ignore them, and a client talking to an older server
    resumes as before, so either side can upgrade first.
  • Fix resuming from a checkpoint older than the last event. The old page's later events made the server's ack look
    invalid, and the session was lost with protocol. Now the client lines up with the server before it sends anything.
    Events queued since the restore move past what the server already has, so they are no longer dropped as duplicates.
    Server events the checkpoint never saw and the server has since forgotten are skipped and reported through
    onError, so the events behind them still arrive. sendEvent's returned sequence number can change for such a
    queued event.
  • A throw or rejected promise from authenticate is now a temporary failure: the socket closes with 1013, the client
    retries, and the session and its seat are kept. Returning null still ends the session. Applies to the Worker
    adapter too.
  • Fix a player kicked while a resume awaited iceServers staying online on a removed session. The client now gets a
    session-unavailable close (4001).
  • Fix a new session being expired during an async iceServers once the server had been up longer than
    resumeGraceMs, which left that player online but never pumped. A new session abandoned during that wait now frees
    its slot at once.

v3.0.1

Choose a tag to compare

@github-actions github-actions released this 04 Nov 22:44
3c934cf
  • Fix sustained underuse of the event byte share when reliable events exceed the burst. Repay bounded one-packet
    debt before the next oversized event instead of repeatedly waiting for a full bucket and clipping pump overshoot.
    Preserve state scheduling, ordered replay, protected credit, rejected-write refunds and the configured byte rate.
  • Add minute-long real-browser state-age and reliable-throughput gates against the unchanged published 2.3 baseline.
    With an explicit throughput-oriented state share of 0.075, five alternating pairs per browser/path deliver lower
    p95 draw-submission state age and at least as many reliable events in every pair. The default remains 0.5; this
    balanced setting does not retain the earlier default-share 66% age benefit. See validation/RELIABLE_PARITY.md.
  • Wire generation 5 and the application APIs are unchanged. This patch is compatible with 3.0.0 peers. Actual-game
    rendering measurements are separate from the transport comparison; no general game-FPS increase is promised.

Sustained latency with reliable throughput

80 valid trials /40 pairs. Five alternating minute-long overload pairs per browser/path, plus normal controls. Unchanged2.3 baseline; candidate state share0.075, default remains0.5.

Browser/path Workload Median p95 age before→after Paired age change Median events before→after Combined parity benefit
chromium/ws normal 59.4→60.0ms 0.9% 0→0 no
chromium/ws mixed-budget 171.8→140.9ms -18.1% 323→331 yes
chromium/rtc normal 59.5→59.9ms 0.7% 0→0 no
chromium/rtc mixed-budget 171.1→141.2ms -17.4% 323→331 yes
firefox/ws normal 60.3→60.0ms -0.5% 0→0 no
firefox/ws mixed-budget 172.7→140.4ms -18.9% 323→331 yes
firefox/rtc normal 59.7→59.4ms 0.4% 0→0 no
firefox/rtc mixed-budget 170.1→140.8ms -17.1% 323→331 yes

Before/after medians and paired median ratios are separate summaries. Age is generation to Canvas draw submission, not physical presentation. Every overload pair must improve age by≥10% beyond both clock estimates and receive at least as many ordered events.

  • Only the mixed-budget setting qualifies; this retains less freshness than the original default-share comparison.
  • Same configured primary application byte rate and burst; bounded oversized-packet debt, exempt control and mirrored datagram bytes are reported separately.
  • No universal network/device/latency guarantee. Actual-game FPS is measured separately.
  • All earlier failed pilots and a rejected state-debt prototype remain separate diagnostics; they are not pooled into these comparisons.

Raw browser evidence · Completed comparison

Actual-game rendering

CS2, Nocturne Keep and kimu completed60 primary rendering trials/30 pairs and same-session RTC reconnects, with unchanged game/rendering code and the exact changed scheduler method isolated on the client. No consistent FPS gain was demonstrated. Nocturne and kimu are near this desktop’s240Hz callback limit at native CPU speed. CS2’s large frame-rate shifts also occur with byte-identical scheduler roles, so its live A/B comparison does not establish a causal gain or regression. All negative pairs, the identical-code control and the scoped CPU-cost diagnostic remain in the evidence. These measurements used the same3.0.0 game authority for both client roles; they do not certify a changed authority’s latency.

Game results and limits · Raw game metrics · Official package file verification

The matching 3.0.1 clients and servers are now deployed for CS2, Nocturne Keep and kimu. Live movement/gameplay and same-player reconnects pass on native RTC and forced WebSocket. Their hosted game gates pass, including ten-minute reconnect/replay soaks. The attached deployed-game-acceptance-3.0.1.json records the tested sources, CI runs and functional acceptance; the separate rendering measurements do not demonstrate a repeatable FPS gain.

v3.0.0

Choose a tag to compare

@github-actions github-actions released this 04 Nov 06:18
14542ed
  • Wire generation 5 adds optional authenticated WebTransport datagrams. Bind each physical HTTP/3 path to the live
    WebSocket session using a short-lived, one-use ticket and epoch; prove both directions before selecting it. Keep
    independently proven RTC and WS fallback, path-size limits and hysteresis. Reliable session/events remain on WS.
    Respect changing datagram limits and provider-owned stream teardown; standby proof clears retry debt too.
    This protocol change requires paired client/server deployment; 2.x clients cannot connect to 3.0 hosts.
  • Add optional bounded metadata tracing for state enqueue, send, receipt, accepted application, playback and explicit
    render submission. Report clock uncertainty and separate queue time from playback age; never retain payloads/secrets.
  • Add bounded cancellable HTTP bulk resources with browser admission/memory limits, optional SHA-256 integrity and a
    Node handler with authorization, origin checks, ETags and backpressure. Bulk bytes stay outside ordered game events.
    Stalled downloads time out, older AbortSignals work, malformed targets return400 and source buffers are snapshotted.
  • Protect state and reliable-event bandwidth under mixed load while lending surplus positive credit. Refund failed
    writes and allow useful smaller states past unsendable large candidates. Preserve ordered event delivery.
  • Add regressions, deterministic comparison with published 2.3.0 and hosted real QUIC/browser and alternating-process
    localhost benchmark gates. Candidate/release evidence is recorded in validation/STATUS.md; no universal speedup claim.
  • Fix mutable Node Buffer views at state/frame/replica ownership boundaries: later caller mutations cannot change
    an already queued or decoded snapshot. Require patched ws8.22+ and patch the pinned local Worker tool's dependencies.

Verified rollout

Matching clients and servers are deployed in CS2, Nocturne Keep, kimu.nyc, Superfighters, Emoji Mahjong, Gallax, Webstrike, Sipoga, Wollosseum, TDBR and Oak0. Public gameplay and reconnect checks pass for all eleven, including private Mahjong hands and seats in phone-sized WebKit.

All four one-hour SDK lifecycle soaks passed. The additional hour-long CS2, Nocturne and kimu game checks passed: CS2 recovered 57 times on each path; Nocturne handled 60 drops and 60 pauses; kimu accounted for all 709,168 online frames with no missing group members. Scheduling stalls and nonzero playback errors remain visible in the game evidence; kimu's raster-suppressed update cadence is not render FPS.

The website passes all 24 pages at desktop and phone widths in light and dark modes, plus all 37 interactive checks. Play the live starter, or download the Node starter: both use 3.0.0 and include an optional bounded timing recorder. A clean public-download install passes native RTC and forced WebSocket movement, reconnect, one queued ping, checkpoint reload and timing-toggle checks. Website and starter verification.

Rendered browser comparison

Protocol and reproduction · Raw evidence · Completed workflow

Five alternating paired trials per workload compare identical scene code on 2.3.0 and 3.0.0: 100 valid trials, 50 pairs. Pixel captures are outside aligned measurement windows. Native RTC requires actual measured RTC application states. The original failed capture attempt is retained separately in the evidence.

Browser / path Workload v2.3 median p95 age v3 median p95 age Median paired age change Median scene cadence change Repeated age benefit
chromium / ws normal 58.7 ms 59.3 ms 0.6% -0.0% no
chromium / ws mixed-budget 168.5 ms 56.8 ms -66.3% 0.0% yes
chromium / ws render-load 61.3 ms 61.7 ms -1.1% -0.4% no
chromium / rtc normal 59.8 ms 60.0 ms -0.0% -0.1% no
chromium / rtc mixed-budget 169.0 ms 58.1 ms -65.6% -0.0% yes
firefox / ws normal 60.4 ms 59.5 ms -1.1% 0.0% no
firefox / ws mixed-budget 170.0 ms 57.2 ms -66.1% 0.0% yes
firefox / ws render-load 64.2 ms 63.7 ms -0.8% 0.0% no
firefox / rtc normal 59.4 ms 59.6 ms 0.4% 0.0% no
firefox / rtc mixed-budget 167.9 ms 57.6 ms -65.9% 0.0% yes

Before/after values are separate medians; the paired-change column summarizes within-pair ratios and can differ from the ratio of those medians.

Age means state generation to JavaScript Canvas draw submission, including application scheduling; it is not packet transit or physical presentation. A repeated benefit requires every pair to improve p95 age by at least 10% beyond the reported clock estimates. No workload demonstrated a consistent scene-cadence gain of at least 5% in every pair.

The mixed-budget case deliberately overloads a 4,000 B/s application budget with reliable events. Median reliable events received during each 12-second window changed from 65 to 60 in all four browser/path combinations. Bounded outboxes rejected hundreds of excess attempts in both versions. Every received/rejected event count and event-age percentile is retained. The 12,000-rectangle workload preserves renderer stalls and is compared over forced WebSocket. These findings support workload-specific improvements, not universally lower latency or higher game FPS.

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 04:44
ce6fbee
  • The server now measures each client's round trip itself: every probeMs (2000 by default, 0 turns it off) it sends a small probe that clients echo at once, and keeps a smoothed session.rttMs and the connection's session.minRttMs. This is the independent observation selectRewindTime asks for: expect a shooter's view half the round trip, its playback delay (which already includes the other half), a frame and its input-queue wait behind now (docs/ADOPTING.md has the model and a calibration). Both hosts (Node and Worker) probe; clients before this release never answer and leave rttMs null, so keep a fallback. Measurements restart on each connection. Wire protocol 4 is unchanged: a 2.2 client ignores probes, and a 2.2 server never sends them.

  • Tests build and pass on Windows: a worker-bundle path no longer goes through URL.pathname, and the 128 Hz pump test drives its game loop by elapsed time, stating that a Windows host's coarse timers cannot run a 128 Hz pump (flush() per tick still carries every state).

  • Restore Safari 15.0–15.3 compatibility for default clones across sub-tick capture/replay, prediction, interpolation, input inboxes and reliable-event checkpoints. Use native structuredClone where available; otherwise clone common structured input data without JSON serialization loss, with explicit failure for unsupported objects. Add missing-native browser and regression coverage; protocol and dependencies are unchanged.

  • Add optional SubtickInputBuffer and replaySubtick netcode helpers: capture monotonic client input edges into fixed-tick fractions and replay segments deterministically without changing simulation tick rate or the wire protocol. Queue bounds, skipped windows, nonmonotonic times and invalid fractions fail explicitly.

  • Add selectRewindTime for Source-style server-derived rewind-time validation. It falls back to an independently estimated server time for implausible client requests only while that estimate is within history. Otherwise it returns timeMs: null and requires explicit game policy; game authority and hit verification remain application responsibilities.

  • Add dedicated edge/boundary/overflow/forged-time regressions and documentation. Freeze replay scheduling and clone input values before invoking game callbacks; reject reentrant input cloning and arithmetic overflow in derived history bounds. Existing transports, games and API defaults are unchanged.

Release validation is recorded in validation/STATUS.md.

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 18:16
64501f4
  • The Worker host explicitly selects ArrayBuffer binary delivery and completes close handshakes on older runtimes. Real workerd regressions failed under the modern Blob default and the older manual-close behavior before this fix; both compatibility dates are release gates.

  • A closing browser socket now holds playback and reports recovery promptly. Heartbeat/handshake timeouts retire and generation-fence the old socket before retrying, so an offline close handshake cannot keep the client apparently online or suppress a resume. Late callbacks from the retired socket cannot end the new connection. A real-socket fault regression failed before this fix.

  • Added named EventClient/createEventServer helpers for migrating existing games. Configured names are independent latest-wins JSON streams with recovery gates; other names are acknowledged events. Handlers, player identity and room membership survive a physical reconnect. Pauses and terminal session expiry have distinct callbacks.

  • Added twinwire/worker, a standard-WebSocket SocketSessionServer for a long-lived Worker or Durable Object instance. It shares the core wire protocol, GameSession and delivery engine with the Node host. It has exact origin policy, bounded admission/rate limits, authentication revalidation, token/room/subject binding, takeover fencing, heartbeat/handshake timeouts and session expiry. It provides no native RTC, durable transport storage or hibernation restoration.

  • New channel/Worker integration tests cover recovery, offline event replay, latest-state readiness, private room broadcast, explicit expiry, takeover and auth revalidation. The auto-connect helper cannot resurrect a client closed during its creation task.

  • Added complete Node and Worker starter projects sharing an interactive two-tab client. They demonstrate authoritative movement, ordered pings, temporary interruption and checkpoint reload. Worker arenas use invite URLs and WebSocket; the Node starter can use native RTC.

Release validation is recorded in validation/STATUS.md. Game migrations and the website rollout have their own deployment receipts.

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 06:03
67d7c0c
  • Snapshot generation age is checked continuously, including while the transport stays online. Old payloads in a
    receive backlog cannot keep advancing remote replay after its delay/prediction budget is exhausted. This holds
    the declared playback time and retains histories; it does not invent missing movement or hide present-position lag.

  • GameClient.receiveState / StateReceiver: shared reconnect recovery for authoritative snapshot streams. It
    resets or retains application histories by policy, holds playback through offline and online-without-fresh-data
    phases, and accepts recovery only after a usable current-connection snapshot on the selected stream. New sessions
    always reset. Idle snapshot headers can release recovery; rejected deltas, queued old state and unrelated streams
    cannot. Raw callbacks and wire protocol 4 are unchanged. The party example uses the shared receiver.

  • AdaptivePlayback.updateDelay(dtMs) separates delay adaptation from cursor movement. The receiver learns valid
    recovery-header lateness while holding its cursor, so a slower new route can recover without advancing old state.

  • A proven RTC recovery now clears retry backoff, so an intermittent network outage does not inherit debt from an
    earlier recovered outage. Duplicate rtc-reset controls for an already-stopped attempt no longer increase that
    debt. This changes no wire bytes or application API. The native gate repeats three recoveries and injects duplicate
    resets; OS-level one-way UDP checks also retain their original failing observation and the candidate rerun.

  • Queued traffic on a socket retired by session takeover is ignored, including a repeated hello. It cannot reclaim
    the player or produce misleading new protocol errors while its close is still in flight. Unauthenticated binary
    traffic on a new connection remains rejected.

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 23:07

For shooters that send a snapshot every tick, and a smaller wire. Protocol version 4: server and clients upgrade
together (a 1.x peer can't talk to a 2.x one).

  • Frame header: 22 bytes to 12 (14 at most): one byte for version and kind, a 16-bit connection epoch, the stream as
    a varint, the 32-bit sequence number, and the time as quarter milliseconds from the connection's time base (sent in
    the welcome). A 64 Hz shooter's 35-byte inputs were more than a third header. The server reconnects a connection
    after four days so its frame times stay in range; the session resumes. Endpoint.sendState refuses a time its frame
    can't carry (more than MAX_TIME_SPAN_MS from the time base) when called, instead of throwing from a later flush.

  • Removed what was only there for compatibility: MAX_DATAGRAM_BYTES (use MTU_STATE_BYTES) and the
    new ReliableEvents(maxEvents, maxBytes) form (pass an options object).

  • GameServer({ pumpHz }): how often queued states and events go out (default 60, 10 to 250). At 64 or 128 Hz
    the default pump coalesced states between pumps, now and then dropping a tick from a stream interpolated two ticks
    behind; set the tick rate here, or session.flush() after each snapshot.

  • PROFILES.fps64 / fps128: a snapshot every tick, the bandwidth for it (96 / 192 KiB/s) and two ticks of render
    delay.

  • AdaptivePlayback.track(timeline, sample, arrival) inserts a sample and counts its lateness, but not a timeline's
    first sample or a duplicate. Observing everything a join hands you (an idle player's state from a second ago) set
    the delay to hundreds of ms for every entity for about 9 s (kimu.nyc: 280 ms after a join, 40 ms with this).

  • ADOPTING: a shooter section (send rate, bandwidth, inputs, datagram size, late states, lag compensation). API: a
    sample's seq is the sender's sequence number, never derived from a time that crossed the wire.

v1.1.2

Choose a tag to compare

@github-actions github-actions released this 25 Oct 10:27
  • Clock across reconnects: a closed socket reset the client's clock, so through every outage serverTime()
    silently became the page's own time, and a playback that never goes backwards could lock onto that timeline and
    freeze after reconnecting to a restarted server (kimu.nyc measured remote players stuck and reversing for 10 s).
    The estimate is now kept through a drop (a resumed session talks to the same server, so the same clock) and starts
    over only with a new session; every welcome still brings a fresh burst of pings.
  • AdaptivePlayback starts over when the clock jumps back by more than a second (a new server) instead of holding
    until the new clock passes the old cursor; a growing delay still never rewinds it.
  • Docs: hold playback until clock.synced, and reset it in onSession when resumed is false.

v1.1.1

Choose a tag to compare

@github-actions github-actions released this 25 Oct 09:48
  • Page unload: bindPageLifecycle() paused the session when a page closed or reloaded, so in a game without
    checkpoints the old player stood frozen for the whole resume grace while the reloaded page joined as someone new.
    Now a real unload (not the back/forward cache) leaves at once, unless the client checkpoints (unload: 'auto', the
    default); unload: 'pause' or 'leave' says so explicitly. Found by kimu.nyc's playtest.

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 09:03

Accuracy improvements measured by porting a 50-player shared world (kimu.nyc) onto Twinwire, plus everything in 1.0.1.

  • Clock sync: a burst of pings after each connect (syncPings 8, syncPingMs 250, then pingMs 1000) instead of
    one a second after the first reply, and NetworkClock jumps to an estimate more than jumpMs (50) away instead of
    slewing for seconds. With 50 clients on an emulated 5G link, the spread of their server-time estimates had stayed at
    20 to 27 ms; every sample's timestamp carries that error, so it widened everyone's interpolation delay.
  • AdaptivePlayback: lateness is a time-decayed histogram (2 ms buckets, halfLifeMs 2000) instead of the last
    128 observations, so its memory is a span of time: fed one observation per snapshot at 60 Hz it had forgotten a
    congested cell's stalls within two seconds and under-sized the delay. latenessMs exposes the covered percentile.
  • Late states (endpoint.lateStates: 'deliver'): a state overtaken by a newer one on its stream is delivered once
    with late: true (within the last 32, de-duplicated; acks still follow the newest), so a 60 Hz snapshot stream with
    jitter fills its interpolation buffer instead of losing every reordered snapshot. Default unchanged ('drop').
  • GameSession.flush() sends now instead of at the next 60 Hz pump. sendEvent, broadcast and Room.send accept
    typed interfaces (JsonInput), still checked when serialized.
  • Impairment: stallMs stalls the WebSocket the way a retransmitted TCP segment does: with probability loss, a
    send and everything queued behind it waits stallMs longer, in order (stats.impairedStalls). Without it, loss
    still drops datagrams only. This makes a lossy cellular network comparable across the two wires in load tests.