HTTP/3 QUIC Inter-Node Transport

Web & API Engineering advanced Building with AI ~25 min read

Overview

QUIC (Quick UDP Internet Connections) is a general-purpose transport-layer protocol built on top of UDP that provides reliable, multiplexed, and encrypted connections between endpoints. Originally developed at Google and standardized as RFC 9000, QUIC eliminates the head-of-line blocking problem inherent in TCP by allowing multiple independent streams to share a single connection without one stalled stream blocking the others. HTTP/3 (RFC 9114) is the application-layer protocol built directly on top of QUIC, replacing the TCP+TLS stack that HTTP/2 relied upon with a fundamentally more efficient transport.

For distributed databases like ExampleDB, inter-node communication is the lifeblood of consensus replication. Traditional TCP-based Raft transports suffer from head-of-line blocking — a single lost packet stalls all in-flight messages on that connection, even those belonging to entirely different Raft groups or log indices. QUIC’s stream-based multiplexing solves this by isolating each logical message stream, so a retransmission on one stream does not delay delivery on another. Combined with 0-RTT connection establishment, connection migration across network changes, and built-in TLS 1.3 encryption, QUIC provides a compelling transport layer for high-throughput Raft replication.

In ExampleDB, the QUIC transport layer maintains dedicated connections to each peer node with 4096-message buffered channels per peer. This buffer depth was chosen to accommodate burst replication traffic during leader elections and log catch-up scenarios without applying backpressure to the Raft state machine. Each peer connection multiplexes append-entries, heartbeats, and snapshot transfers across independent QUIC streams, ensuring that a large snapshot transfer never blocks time-sensitive heartbeat delivery. The result is a transport layer that maintains sub-millisecond heartbeat latency even under heavy replication load.

Core Concepts

QUIC Transport Protocol

QUIC operates over UDP and provides reliable, ordered delivery with built-in TLS 1.3 encryption. Unlike TCP, QUIC connections are identified by a connection ID rather than the IP/port 4-tuple, enabling seamless connection migration. Each QUIC connection supports thousands of concurrent streams, each with independent flow control and loss recovery. For ExampleDB, this means a single QUIC connection to each peer can carry all Raft message types simultaneously without interference.

Stream Multiplexing

QUIC streams are lightweight, independently flow-controlled channels within a single connection. Streams can be unidirectional or bidirectional, and creating a new stream is nearly free — no additional handshake is required. In the ExampleDB transport, each Raft message type (heartbeats, append-entries, vote requests, snapshot chunks) is dispatched to its own stream priority class, preventing large log replication batches from starving time-critical heartbeat messages. Stream-level flow control ensures no single message type can monopolize the connection’s bandwidth.

0-RTT Connection Establishment

When a QUIC client reconnects to a server it has previously communicated with, it can send application data in the very first packet — achieving zero round-trip-time (0-RTT) connection establishment. This is critical for ExampleDB’s inter-node transport because cluster membership changes and node restarts must re-establish replication channels with minimal latency. The 0-RTT capability means a restarted node can begin sending Raft messages to its peers before even completing the full handshake, reducing election timeout sensitivity to network latency.

Connection Migration

QUIC connections survive network address changes because they are identified by connection IDs rather than IP addresses and port numbers. When a node’s network interface changes — common in cloud environments with dynamic IP assignment or container orchestration — the QUIC connection continues without interruption. For ExampleDB deployments on Kubernetes or across availability zones, connection migration prevents spurious leader elections that would otherwise be triggered by brief network path changes during pod rescheduling or live migration events.

Per-Peer Message Buffers

ExampleDB maintains a 4096-message buffered channel for each peer connection. This buffer absorbs replication bursts — such as when a newly elected leader replicates uncommitted entries to all followers, or when a slow follower catches up after a network partition heals. The 4096 depth balances memory usage (approximately 32 MB per peer assuming 8 KB average message size) against the risk of applying backpressure to the Raft state machine, which could cascade into missed heartbeats and unnecessary elections.

How It Works

QUIC Listener and Dialer Setup

The foundation of ExampleDB’s QUIC transport is a listener on each node that accepts incoming peer connections, and a dialer that initiates outbound connections. The quic-go library provides the Go implementation of QUIC used throughout the transport layer.

package transport

import (
    "context"
    "crypto/tls"
    "fmt"
    "sync"

    "github.com/quic-go/quic-go"
)

// QUICTransport manages QUIC connections to peer nodes
// for Raft replication in ExampleDB.
type QUICTransport struct {
    mu        sync.RWMutex
    listener  *quic.Listener
    peers     map[uint64]*PeerConn
    tlsConf   *tls.Config
    quicConf  *quic.Config
    bufSize   int
    localAddr string
}

// NewQUICTransport creates a transport with per-peer
// buffered channels of the specified depth.
func NewQUICTransport(addr string, tlsConf *tls.Config) (*QUICTransport, error) {
    quicConf := &quic.Config{
        MaxIdleTimeout:        30 * time.Second,
        KeepAlivePeriod:       5 * time.Second,
        MaxIncomingStreams:     1024,
        MaxIncomingUniStreams:  1024,
        Allow0RTT:             true,
        EnableDatagrams:       true,
    }

    listener, err := quic.ListenAddr(addr, tlsConf, quicConf)
    if err != nil {
        return nil, fmt.Errorf("quic listen on %s: %w", addr, err)
    }

    return &QUICTransport{
        listener:  listener,
        peers:     make(map[uint64]*PeerConn),
        tlsConf:   tlsConf,
        quicConf:  quicConf,
        bufSize:   4096,
        localAddr: addr,
    }, nil
}

// AcceptLoop handles incoming peer connections.
func (t *QUICTransport) AcceptLoop(ctx context.Context) error {
    for {
        conn, err := t.listener.Accept(ctx)
        if err != nil {
            return fmt.Errorf("accept quic conn: %w", err)
        }
        go t.handleInbound(ctx, conn)
    }
}

Stream-Based Raft Message Transport

Each Raft message is sent over a dedicated QUIC stream. The transport opens a new stream for each message, writes the framed payload, and closes the send side. This pattern leverages QUIC’s cheap stream creation to avoid head-of-line blocking between messages of different priorities.

// PeerConn wraps a QUIC connection with a buffered
// message channel for outbound Raft messages.
type PeerConn struct {
    nodeID  uint64
    conn    quic.Connection
    sendCh  chan RaftMessage
    stopCh  chan struct{}
}

// RaftMessage represents a framed Raft protocol message.
type RaftMessage struct {
    Type    uint8
    Term    uint64
    Index   uint64
    Payload []byte
}

// NewPeerConn initializes a peer connection with a
// 4096-message buffer for backpressure management.
func NewPeerConn(nodeID uint64, conn quic.Connection) *PeerConn {
    pc := &PeerConn{
        nodeID: nodeID,
        conn:   conn,
        sendCh: make(chan RaftMessage, 4096),
        stopCh: make(chan struct{}),
    }
    go pc.sendLoop()
    return pc
}

// Send enqueues a Raft message for transmission. Returns
// false if the buffer is full, signaling backpressure.
func (pc *PeerConn) Send(msg RaftMessage) bool {
    select {
    case pc.sendCh <- msg:
        return true
    default:
        // Buffer full — apply backpressure
        return false
    }
}

// sendLoop drains the buffered channel and writes each
// message to a new QUIC stream.
func (pc *PeerConn) sendLoop() {
    for {
        select {
        case msg := <-pc.sendCh:
            if err := pc.writeMessage(msg); err != nil {
                pc.reconnect()
            }
        case <-pc.stopCh:
            return
        }
    }
}

func (pc *PeerConn) writeMessage(msg RaftMessage) error {
    ctx, cancel := context.WithTimeout(
        context.Background(), 5*time.Second,
    )
    defer cancel()

    stream, err := pc.conn.OpenStreamSync(ctx)
    if err != nil {
        return fmt.Errorf("open stream to node %d: %w",
            pc.nodeID, err)
    }
    defer stream.Close()

    // Write framed message: [type(1)][term(8)][index(8)][len(4)][payload]
    frame := encodeRaftFrame(msg)
    _, err = stream.Write(frame)
    return err
}

Connection Pool Management

The transport maintains a pool of QUIC connections to peer nodes, lazily establishing connections on first use and re-establishing them after failures. The pool uses the 0-RTT capability for fast reconnection.

// GetOrDial retrieves an existing peer connection or
// establishes a new one using 0-RTT when available.
func (t *QUICTransport) GetOrDial(
    ctx context.Context,
    nodeID uint64,
    addr string,
) (*PeerConn, error) {
    t.mu.RLock()
    if pc, ok := t.peers[nodeID]; ok {
        t.mu.RUnlock()
        return pc, nil
    }
    t.mu.RUnlock()

    t.mu.Lock()
    defer t.mu.Unlock()

    // Double-check after acquiring write lock
    if pc, ok := t.peers[nodeID]; ok {
        return pc, nil
    }

    // Attempt 0-RTT early data connection
    conn, err := quic.DialAddrEarly(
        ctx, addr, t.tlsConf, t.quicConf,
    )
    if err != nil {
        return nil, fmt.Errorf(
            "dial node %d at %s: %w", nodeID, addr, err,
        )
    }

    pc := NewPeerConn(nodeID, conn)
    t.peers[nodeID] = pc
    return pc, nil
}

// RemovePeer closes and removes a peer connection,
// draining any buffered messages gracefully.
func (t *QUICTransport) RemovePeer(nodeID uint64) {
    t.mu.Lock()
    defer t.mu.Unlock()

    if pc, ok := t.peers[nodeID]; ok {
        close(pc.stopCh)
        pc.conn.CloseWithError(0, "peer removed")
        delete(t.peers, nodeID)
    }
}

// Stats returns per-peer buffer utilization metrics
// for monitoring and alerting on backpressure.
func (t *QUICTransport) Stats() map[uint64]PeerStats {
    t.mu.RLock()
    defer t.mu.RUnlock()

    stats := make(map[uint64]PeerStats, len(t.peers))
    for id, pc := range t.peers {
        stats[id] = PeerStats{
            NodeID:      id,
            BufferUsed:  len(pc.sendCh),
            BufferCap:   cap(pc.sendCh),
            BufferPct:   float64(len(pc.sendCh)) / 4096.0 * 100,
        }
    }
    return stats
}

Implementation Patterns

Connection Pooling

ExampleDB maintains exactly one QUIC connection per peer node, multiplexing all Raft traffic over that connection’s streams. This differs from TCP-based transports that often use multiple connections per peer to work around head-of-line blocking. With QUIC, a single connection provides better congestion control signals and lower resource overhead. The connection pool lazily initializes connections on first use and performs background health checks using QUIC keep-alive frames configured at 5-second intervals.

Backpressure with 4096-Message Buffers

The 4096-message buffer per peer serves as the primary backpressure mechanism. When the buffer fills — typically because a peer is unreachable or processing slowly — the Send method returns false immediately rather than blocking. This non-blocking design prevents a slow peer from stalling the Raft state machine, which must continue processing messages for other peers. The Raft layer interprets a full buffer as a signal to mark the peer as potentially unreachable and begin coalescing messages. Buffer utilization metrics are exposed via the Stats() method for Prometheus-based alerting, with a recommended warning threshold at 75% (3072 messages) and a critical threshold at 95% (3891 messages).

Graceful Degradation to TCP

Not all network environments support UDP traffic reliably. Corporate firewalls, certain cloud VPC configurations, and legacy network appliances may drop or rate-limit UDP packets. ExampleDB’s transport layer implements a fallback strategy: if a QUIC connection cannot be established within 3 seconds, the transport falls back to a gRPC-over-TCP connection for that specific peer. The fallback is per-peer, so nodes with working QUIC paths continue to benefit from stream multiplexing while degraded peers use TCP. A background probe periodically attempts to re-establish QUIC connectivity, promoting the peer back to the faster transport when the path clears.

Stream Priority Classes

ExampleDB assigns Raft message types to three priority classes on the QUIC connection. Heartbeat and vote messages are assigned the highest priority, ensuring they are never delayed by bulk data transfers. Append-entries messages for committed log indices receive medium priority, while snapshot chunks and large log catch-up batches receive the lowest priority. QUIC’s stream-level flow control ensures that low-priority transfers yield bandwidth to high-priority streams under congestion, maintaining consistent heartbeat latency even during full snapshot transfers that may move gigabytes of data.

Practical Applications

Raft Replication in ExampleDB

The QUIC transport is the default inter-node communication layer for ExampleDB’s Raft consensus implementation. In a typical 5-node cluster, each node maintains 4 QUIC connections (one to each peer) with 4096-message buffers, for a total of 16,384 buffered messages across the cluster per node. During steady-state operation with a write-heavy workload, the transport delivers approximately 150,000 Raft messages per second per connection, with p99 latency under 2 milliseconds for heartbeat delivery.

Leader Election Performance

QUIC’s 0-RTT connection re-establishment is particularly valuable during leader elections. When a leader fails and a follower initiates an election, it must rapidly send vote requests to all peers. If any peer connections were recently disrupted (as often happens when the leader’s failure was caused by a network partition), the 0-RTT capability allows the candidate to send vote requests in its very first packet to the reconnecting peer. In benchmarks, this reduces median election completion time from 320 ms (TCP with TLS handshake) to 180 ms (QUIC with 0-RTT), a 44% improvement that directly reduces write unavailability windows.

Cross-Region Deployments

For ExampleDB clusters spanning multiple cloud regions, the QUIC transport’s connection migration capability prevents false leader elections during network path changes. Cloud providers frequently reroute traffic between regions, causing brief IP address changes that would reset TCP connections and trigger Raft election timeouts. QUIC connections survive these transitions transparently, maintaining replication continuity across regions with high-latency, variable-path network links.

Common Pitfalls

Further Reading