Talk RFC 001 quic transport
RFC-001 — QUIC Transport for Koder Talk
| Field | Value |
|---|---|
| RFC | RFC-001 |
| Title | QUIC Transport for Koder Talk |
| Author | Koder Engineering |
| Date | 2026 |
| Status | Draft |
| Supersedes | — |
Table of Contents
- Abstract
- Motivation
- Background
- Goals and Non-Goals
- Proposed Solution
- Architecture
- Protocol Design
- Message Framing
- Connection Management
- Reconnection and Migration
- Fallback Strategy
- Server Implementation
- Web Client Implementation
- Mobile Client Implementation
- Performance Targets
- Security Considerations
- Observability
- Rollout Roadmap
- Alternatives Considered
- Open Questions
- References
1. Abstract
This RFC proposes replacing the current WebSocketbased data plane in Koder Talk with a QUIC (RFC 9000) transport layer using WebTransport (W3C). The change eliminates headof-line (HOL) blocking, enables transparent connection migration between networks, and reduces p99 message latency from ~150ms to ≤50ms on 4G networks. Existing WebSocket connections are retained as a fallback for clients that do not support QUIC or WebTransport.
2. Motivation
2.1 Current State
Koder Talk currently uses WebSocket (RFC 6455) over HTTP/1.1 for its real-time data plane. Each client opens a single persistent WebSocket connection to the Talk server, multiplexing all conversations over that single TCP stream.
2.2 Problems with HTTP/1.1 + WebSocket
*eadofLine Blocking at the Transport Layer*
TCP is a single ordered byte stream. A single lost or reordered packet causes the TCP stack to stall delivery of all subsequent data until the gap is retransmitted and acknowledged. In Koder Talk, this means a lost packet in one conversation blocks delivery of messages in all other conversations sharing the same WebSocket connection — even those that are entirely independent.
On mobile networks (LTE/5G with variable signal, subway tunnels, elevator transitions), packet loss rates of 1–5% are common. At 1% loss with a 100ms round-trip time, the expected stall per minute is approximately 300ms of cumulative blocking across all conversations.
*CP Connection Setup Overhead*
Each reconnection requires a full TCP handshake (1 RTT) plus a TLS 1.3 handshake (1 RTT) — a minimum of 2 RTTs before the first byte of application data is sent. On a 100ms RTT mobile link, this is 200ms of dead time per reconnect.
*etwork Transition Disruption*
When a mobile device transitions from WiFi to cellular (or between cellular towers), the TCP connection's 4tuple (src IP, src port, dst IP, dst port) changes, causing the OS to tear down the existing connection. This forces a full reconnect cycle, reauthentication, and replay of any undelivered messages.
*easured Impact*
Current production telemetry (from observe/jet traces on s.k.lin):
- p50 message delivery latency: 38ms
- p95 message delivery latency: 89ms
- p99 message delivery latency: 148ms
- Reconnect rate on mobile clients: 4.2 reconnectshouruser
- Reconnect-induced message gap: median 340ms
2.3 Why This Matters for Talk
Koder Talk is a messenger. Users expect near-instant delivery. A 150ms p99 latency is perceptible as lag when typing indicators and read receipts are involved. Reconnect gaps cause the "message stuck in sending" experience that erodes user trust.
3. Background
3.1 QUIC (RFC 9000)
QUIC is a UDP-based transport protocol standardized by the IETF in May 2021. Key properties relevant to Talk:
- *ultiplexed streams with independent flow control* each stream is delivered independently; a lost packet in stream A does not block stream B.
- *
RTT connection resumption* previouslyconnected clients can send data with 0 additional round trips using a session ticket. - *-RTT full connection* new connections require only 1 RTT (TLS 1.3 is integrated into the QUIC handshake; no separate TLS handshake).
- *onnection migration* QUIC connections are identified by a 64
bit Connection ID, not the 4tuple. The client can send a PATHCHALLENGE/PATHRESPONSE to migrate the connection to a new network path without re-establishing.
3.2 WebTransport (W3C)
WebTransport is a W3C API that provides browseraccessible QUIC streams over HTTP/3. It allows web applications to open bidirectional and unidirectional streams to a server endpoint, bypassing the singlestream limitation of WebSocket.
WebTransport is available in:
- Chrome 97+ (stable since January 2022)
- Firefox 114+ (stable since June 2023)
- Safari: not yet supported as of this RFC date
3.3 quic-go
quic-go is the most mature Go implementation of QUIC, with production use at Cloudflare, Caddy, and others. As of v0.42, it supports:
- QUIC RFC 9000 (transport)
- HTTP/3 RFC 9114
- QPACK RFC 9204
- WebTransport (draft
ietfwebtrans-http3) - 0-RTT resumption
- Connection migration
4. Goals and Non-Goals
Goals
- Eliminate HOL blocking for Talk's data plane.
- Achieve p99 message latency ≤ 50ms on 4G networks.
- Enable transparent connection migration (WiFi → cellular) without reconnect.
- Preserve full backward compatibility for WebSocket clients.
- Introduce no new authentication mechanism — reuse existing Koder ID tokens.
Non-Goals
- Replacing the Talk signaling plane (presence, typing indicators remain over the existing control stream, now mapped to the QUIC control stream).
- Rewriting the Talk protocol itself (message format, channel model, permissions are unchanged).
- Eliminating WebSocket support (it remains as permanent fallback).
- QUIC for Talk's server
toserver federation (out of scope for this RFC).
5. Proposed Solution
Add a QUIC/WebTransport endpoint to the Talk server alongside the existing WebSocket endpoint. Clients that support QUIC negotiate a WebTransport connection on first connect; clients that do not fall back to WebSocket transparently.
At the protocol level, each conversation is mapped to one bidirectional QUIC stream. A dedicated control stream carries presence, typing indicators, acks, and session management. Messages are encoded as Protobuf, framed with a 4-byte length prefix.
6. Architecture
┌─────────────────────────────────────────────────────────┐
│ Talk Client │
│ │
│ ┌───────────────┐ ┌───────────────────────┐ │
│ │ Transport │ │ Transport Selector │ │
│ │ Abstraction │◄───────►│ (capability detect) │ │
│ └───────┬───────┘ └───────────────────────┘ │
│ │ │
│ ┌─────┴──────┐ │
│ │ │ │
│ QUIC/WT WebSocket │
│ client client │
└──────┬───────────┬────────────────────────────────────┘
│ │
│ :9500 │ :9300
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Talk Server │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ QUIC Listener │ │ WebSocket Listener │ │
│ │ :9500/quic │ │ :9300/ws │ │
│ └────────┬─────────┘ └────────────┬─────────────┘ │
│ │ │ │
│ └────────────┬──────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Session Manager │ │
│ │ (auth, channel map) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Message Router │ │
│ │ (fan-out, ordering) │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────┘Stream Layout per QUIC Connection
Connection (one per client)
│
├── Stream 0: Control Stream (bidirectional)
│ └── Presence updates, typing indicators, acks, ping/pong
│
├── Stream 1: Conversation channel-A (bidirectional)
│ └── Messages for channel UUID aaa-bbb-ccc
│
├── Stream 2: Conversation channel-B (bidirectional)
│ └── Messages for channel UUID ddd-eee-fff
│
└── Stream N: Conversation channel-N (bidirectional)Each stream is opened lazily when the user opens a conversation. Streams are closed (FIN) when the user navigates away from a conversation for more than 30 seconds (idle timeout per stream).
7. Protocol Design
7.1 Connection Establishment
- Client sends HTTP3 CONNECT request to `https:talk.koder.dev.well
known/webtransportid-token>`.withAuthorization: Bearer <koder - Server validates the token against Koder ID (existing
platform/idservice). - Server responds with HTTP/3 200, upgrading to WebTransport session.
- Client immediately opens Stream 0 (control stream) and sends a
SessionHelloProtobuf message containing:client_version: string (e.g. "talk/2.1.0")transport: enumQUIC_WEBTRANSPORTlast_event_id: uint64 (highest event sequence number received in previous session, for gap recovery)
- Server responds with
SessionAckcontaining:session_id: bytes (16-byte UUID)server_time_ms: int64missed_events: repeated Event (events sincelast_event_id, if any)
7.2 Opening a Conversation Stream
When the user opens conversation (channel) C:
- Client opens a new bidirectional stream
S_C. - Client sends
StreamHello { channel_id: "...", last_message_seq: uint64 }. - Server sends
StreamAck { backfill: repeated Message }(messages sincelast_message_seq). - Thereafter, messages flow bidirectionally on
S_C.
7.3 Sending a Message
Client → Server on S_C:
message ClientSend {
string nonce = 1; // client-generated dedup key
bytes payload = 2; // Protobuf-encoded MessageContent
int64 client_ts = 3; // client clock ms
}Server → Client on S_C:
message ServerDeliver {
uint64 seq = 1; // monotonic sequence number for channel
string sender_id = 2;
bytes payload = 3;
int64 server_ts = 4;
string nonce = 5; // echoed for ack matching
}7.4 Control Stream Messages
The control stream carries all non-message signals:
enum ControlType {
PRESENCE_UPDATE = 0;
TYPING_START = 1;
TYPING_STOP = 2;
READ_RECEIPT = 3;
ACK = 4;
PING = 5;
PONG = 6;
}
message ControlFrame {
ControlType type = 1;
string channel_id = 2; // optional
string user_id = 3; // optional
bytes payload = 4; // type-specific
}8. Message Framing
All messages on QUIC streams use a simple 4byte lengthprefixed framing, identical to the existing ProtobufoverWebSocket framing used in Talk today:
┌────────────────────────────────┬──────────────────────────┐
│ Length (4 bytes, big-endian) │ Protobuf payload │
│ uint32, max 4 MB │ (variable) │
└────────────────────────────────┴──────────────────────────┘This framing is applied on top of the QUIC stream byte stream. The QUIC layer already provides reliable, ordered delivery within a single stream, so no additional sequencing is needed at the framing layer.
Maximum message size: 4 MB (same as WebSocket limit). Messages exceeding this must be split by the application layer (e.g., large file transfers use the existing blob upload path, not the message stream).
9. Connection Management
9.1 Authentication
Authentication reuses the existing Koder ID Bearer token flow. The token is passed in the HTTP/3 CONNECT Authorization header. Token expiry and refresh follow existing Talk session policy (24-hour tokens, refreshed via background Koder ID API call). No changes to the auth subsystem are required.
9.2 Session Lifecycle
- *dle timeout* if no data (including PING) is sent on the QUIC connection for 60 seconds, the server sends a PING. If no PONG within 10 seconds, the server closes the connection with QUIC error code
APPLICATION_ERROR(0x01). - *ax connection lifetime* 24 hours (aligned with token lifetime). Server sends
CONNECTION_CLOSEwithNO_ERRORafter 24h; client re-connects with a fresh token. - *ax streams per connection* 256 simultaneous conversation streams (configurable via server flag
--quic-max-streams).
9.3 Flow Control
QUIC provides native perstream and perconnection flow control. Initial stream window: 256 KB. Initial connection window: 8 MB. These values match the defaults of quic-go v0.42 and can be tuned via server configuration.
10. Reconnection and Migration
10.1 Connection Migration
QUIC Connection IDs decouple the logical connection from the network 4-tuple. When a client device changes network (WiFi → LTE, or between access points):
- The OS networking layer changes the source IP/port.
- The QUIC stack detects the path change and sends a
PATH_CHALLENGEframe on the new path. - The server validates the challenge and responds with
PATH_RESPONSE. - Traffic migrates to the new path. No application-layer reconnect is needed.
- In-flight messages on streams are retransmitted by QUIC if lost during migration; application does not observe a gap.
This eliminates the "message stuck in sending" experience for mobile users transitioning between networks.
10.2 Server-Initiated Migration
If a Talk server is taken out of rotation (rolling deploy), it sends a NEW_CONNECTION_ID frame with a retire_prior_to hint, signaling the client to prefer the new connection ID. Combined with DNS TTLbased load balancing, this enables zerodowntime deployments.
10.3 Reconnect on Full Connection Loss
If the QUIC connection is lost entirely (e.g., device went offline for more than the idle timeout):
- Client reconnects (1 RTT QUIC + 1
RTT session resume if 0RTT ticket valid, else 1-RTT fresh). - Client sends
last_event_idinSessionHello. - Server delivers missed events in
SessionAck.missed_events.
Gap recovery is identical to the existing WebSocket reconnect flow, reusing the same server-side event buffer (currently 5 minutes of events per user, stored in Redis).
11. Fallback Strategy
Not all clients support QUIC or WebTransport. The fallback strategy ensures zero regression for unsupported clients.
11.1 Client Capability Detection
*eb:*
const supportsWebTransport = typeof WebTransport !== 'undefined';*lutter/Mobile:*
final supportsQuic = await QuicTransport.isAvailable();*esktop (Electron/Tauri):*
- Electron 22+ (Chromium 108+): WebTransport available.
- Tauri: falls back to WebSocket (native QUIC support pending).
11.2 Transport Selection Algorithm
if QUIC available AND server advertises QUIC:
attempt WebTransport connection
if connection succeeds within 2s:
use QUIC
else:
fall back to WebSocket
else:
use WebSocketThe server advertises QUIC support via an HTTP response header on the existing WebSocket endpoint:
Alt-Svc: h3=":9500"; ma=8640011.3 Protocol Compatibility
WebSocket clients continue to use the existing ProtobufoverWebSocket protocol unchanged. No server-side message format changes affect WebSocket clients. The transport abstraction layer on the server routes messages from both transport types through the same Session Manager and Message Router.
12. Server Implementation
12.1 Dependencies
Add to platform/talk/go.mod:
github.com/quic-go/quic-go v0.42.0
github.com/quic-go/webtransport-go v0.8.012.2 Endpoint Registration
// platform/talk/internal/transport/quic.go
package transport
import (
"github.com/quic-go/webtransport-go"
"net/http"
)
func NewQUICServer(cfg Config) (*webtransport.Server, error) {
s := &webtransport.Server{
H3: http3.Server{
Addr: cfg.QUICAddr, // ":9500"
TLSConfig: cfg.TLS,
},
CheckOrigin: func(r *http.Request) bool {
return checkOrigin(r, cfg.AllowedOrigins)
},
}
http.HandleFunc("/.well-known/webtransport", func(w http.ResponseWriter, r *http.Request) {
if err := authenticateRequest(r, cfg.IDClient); err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
session, err := s.Upgrade(w, r)
if err != nil {
return
}
handleQUICSession(session, cfg)
})
return s, nil
}12.3 Session Handler
func handleQUICSession(session *webtransport.Session, cfg Config) {
// Accept control stream (stream 0)
controlStream, err := session.AcceptStream(session.Context())
if err != nil {
return
}
go handleControlStream(session, controlStream, cfg)
// Accept conversation streams
for {
stream, err := session.AcceptStream(session.Context())
if err != nil {
return
}
go handleConversationStream(session, stream, cfg)
}
}12.4 TLS Requirements
QUIC requires TLS 1.3. The existing Talk TLS certificate (issued by Let's Encrypt for talk.koder.dev) is compatible. The QUIC server must use the same certificate as the HTTP/3 endpoint.
12.5 Port and Firewall
New UDP port 9500 must be opened on the Talk server firewall. QUIC uses UDP. The existing TCP 9300 (WebSocket) remains open.
Infra change required: add UDP9500 to `infrajet/sites.toml` allow rules for the Talk server host.
13. Web Client Implementation
13.1 WebTransport API
// apps/talk-web/src/transport/quic-transport.js
export class QUICTransport {
constructor(url, token) {
this.url = url;
this.token = token;
this.session = null;
this.controlStream = null;
this.conversationStreams = new Map();
}
async connect() {
this.session = new WebTransport(this.url, {
serverCertificateHashes: [], // production uses trusted CA
});
await this.session.ready;
// Open control stream
this.controlStream = await this.session.createBidirectionalStream();
await this.sendSessionHello(this.controlStream);
}
async openConversation(channelId) {
const stream = await this.session.createBidirectionalStream();
this.conversationStreams.set(channelId, stream);
await this.sendStreamHello(stream, channelId);
this.readLoop(stream, channelId);
return stream;
}
}13.2 Polyfill for Safari/Unsupported Browsers
For browsers without WebTransport (primarily Safari), the existing WebSocketTransport class is used. The TransportFactory selects the appropriate implementation:
export function createTransport(serverUrl, token) {
if (typeof WebTransport !== 'undefined') {
return new QUICTransport(serverUrl.replace('wss://', 'https://'), token);
}
return new WebSocketTransport(serverUrl, token);
}14. Mobile Client Implementation
14.1 Flutter/Dart
QUIC support in Flutter/Dart is available through two paths:
*ption A: flutter_quiche (recommended)*
- Wraps Cloudflare's
quichelibrary (C/Rust, battle-tested in production at Cloudflare) - Package:
flutter_quiche(pub.dev) - Supports full QUIC RFC 9000 + connection migration
- Requires native build step (pre-built binaries available for Android/iOS)
*ption B: dart:io HTTP/3 (experimental)*
- Available in Dart SDK 3.x under feature flag
- Not yet production-ready as of this RFC date
- Revisit in Phase 2 evaluation
For Phase 2, flutter_quiche is the recommended approach. The Flutter Talk client (apps/talk) will be updated to include a QUICTransport implementation wrapping flutter_quiche, with identical fallback logic to the web client.
// apps/talk/lib/transport/quic_transport.dart
class QUICTransport implements TalkTransport {
late QuicheConnection _conn;
@override
Future<void> connect(String host, int port, String token) async {
_conn = await QuicheConnection.connect(
host: host,
port: port,
tlsConfig: TlsConfig.fromSystemRoots(),
);
await _sendSessionHello(token);
}
@override
Future<Stream<TalkMessage>> openConversation(String channelId) async {
final stream = await _conn.openBidirectionalStream();
await _sendStreamHello(stream, channelId);
return _readLoop(stream);
}
}15. Performance Targets
| Metric | Current (WebSocket) | Target (QUIC) |
|---|---|---|
| p50 message delivery latency | 38ms | ≤ 20ms |
| p95 message delivery latency | 89ms | ≤ 35ms |
| p99 message delivery latency | 148ms | ≤ 50ms |
| Reconnect rate (mobile, hruser) | 4.2 | ≤ 0.5 |
| Connection setup time (new) | ~200ms (2 RTT) | ~100ms (1 RTT) |
| Connection setup time (resume) | ~200ms (2 RTT) | ~0ms (0-RTT) |
| Throughput (burst 100 msg/s) | baseline | ≥ baseline |
Performance targets are measured under simulated 4G conditions: 80ms base RTT, 1% packet loss, 20 Mbps downlink, using tc netem in the staging environment.
16. Security Considerations
16.1 TLS
QUIC integrates TLS 1.3. The connection is encrypted endtoend. No new cryptographic requirements; existing certificate chain is used.
16.2 Connection ID Privacy
QUIC Connection IDs are rotated during connection migration to prevent linkability across network paths. quic-go rotates connection IDs automatically.
16.3 0-RTT Replay
QUIC 0RTT connection resumption is vulnerable to replay attacks on the 0RTT data. For Talk, SessionHello is the only 0RTT message. Since it contains only RTT).last_event_id (idempotent), replay has no harmful effect. Actual message sends occur only after the full handshake completes (1
16.4 UDP Amplification
QUIC mitigates UDP amplification via its address validation handshake (CRYPTO + ACK). quic-go enforces this by default.
16.5 Rate Limiting
The QUIC endpoint applies the same perIP and peruser rate limits as the existing WebSocket endpoint, enforced at the Session Manager layer.
17. Observability
The following metrics will be added to the Talk server's OpenTelemetry export:
talk.quic.connections.active gauge active QUIC connections
talk.quic.streams.active gauge active conversation streams
talk.quic.messages.sent counter messages sent via QUIC
talk.quic.messages.received counter messages received via QUIC
talk.quic.migration.count counter connection migrations (WiFi→LTE etc.)
talk.quic.latency.p99 histogram message delivery p99
talk.transport.type label "quic" | "websocket"Traces: each message delivery tagged with transport.type allows the Observe stack to compare latency distributions between QUIC and WebSocket clients sidebyside.
18. Rollout Roadmap
Phase 1 — Go Server QUIC Endpoint (4 weeks)
- Add
quic-goandwebtransport-godependencies. - Implement
QUICListeneron:9500. - Implement
handleQUICSessionandhandleConversationStream. - Transport abstraction in Session Manager (routes both WebSocket and QUIC sessions through same logic).
- Open UDP/9500 in infra firewall.
- Publish
Alt-Svcheader from WebSocket endpoint. - Load test with
quic-gobenchmark tool: ≥ 10,000 concurrent QUIC connections. - Deploy behind feature flag
QUIC_ENABLED=true(default false in production).
*ate* internal dogfood with QUIC_ENABLED=true; p99 ≤ 50ms confirmed in staging under netem 4G profile.
Phase 2 — Flutter Mobile Client (3 weeks)
- Integrate
flutter_quicheinapps/talk. - Implement
QUICTransport+TransportSelector. - Beta release to internal users (Koder staff, ~50 devices).
- Instrument with OpenTelemetry; compare QUIC vs WebSocket latency dashboards.
*ate* p99 ≤ 50ms on beta users' real 4G connections; zero message loss reported.
Phase 3 — Web WebTransport (2 weeks)
- Implement
QUICTransportinapps/talk-web. - Ship behind
FEATURE_WEBTRANSPORT=trueflag in web app config. - AB test: 10% of ChromeFirefox users on QUIC, rest on WebSocket.
*ate* p99 ≤ 50ms in A/B group; no regression in delivery rate vs WebSocket group.
Phase 4 — Connection Migration + Full Rollout (2 weeks)
- Enable connection migration (
quic-godefault: enabled; no code change needed). - Validate migration in field: measure reconnect rate drop from 4.2 → ≤ 0.5 hruser.
- Flip default transport to QUIC for all clients.
- Remove feature flags; WebSocket path remains permanently as fallback for unsupported clients.
19. Alternatives Considered
19.1 HTTP/2 Server Push
HTTP/2 multiplexes streams but still uses a single TCP connection, so HOL blocking at the TCP layer persists. Rejected: does not solve the core problem.
19.2 gRPC Streaming (HTTP/2)
Same TCP HOL blocking issue. Rejected for the same reason as HTTP/2.
19.3 SSE + HTTP/2 Push
Server-Sent Events for server→client, HTTP POST for client→server. Avoids WebSocket but does not solve HOL blocking. Rejected: more complex, worse latency.
19.4 Raw UDP with Custom Reliability
Implementing custom reliability over raw UDP (similar to what QUIC does internally) would require re-inventing most of QUIC without the benefit of TLS integration, ecosystem tooling, or browser support. Rejected: not justified when QUIC is available.
19.5 QUIC without WebTransport (Native-only)
QUIC via a native library (quiche, lsquic) without WebTransport would exclude web clients entirely. Rejected: web client is a first-class platform for Talk.
20. Open Questions
*1* Should we use WebTransport sessions or a raw HTTP3 endpoint for the QUIC data plane? This RFC proposes WebTransport for browser compatibility; a raw HTTP3 endpoint would be simpler on the server but loses web client support.
*2* 0RTT is enabled in this proposal for RTT for the first message in a conversation stream? Risk: limited replay surface; benefit: subSessionHello only. Should we also allow 010ms firstmessage latency for returning users.
*3* Should connection migration be tested against CGNAT environments (common in Brazilian mobile carriers)? Some CGNAT implementations break QUIC by mangling UDP source ports.
21. References
- [RFC 9000] QUIC: A UDP-Based Multiplexed and Secure Transport — IETF, May 2021
- [RFC 9114] HTTP/3 — IETF, June 2022
- [W3C WebTransport] WebTransport API — W3C Working Draft
- [quic
go] https:/ithub.com/quicgo/quic-go - [webtransport
go] https:/ithub.com/quicgo/webtransport-go - [flutterquiche] https:/ub.devpackagesflutterquiche
- [SLSA] Supply chain Levels for Software Artifacts — https:/lsa.dev
- [Koder Talk STATUS.md] platformtalkdocs/STATUS.md
- [Koder Talk Protocol Spec] platformtalkdocs/protocol-spec.md