QUIC’s unreliable datagrams are actually more reliable for real-time data than TCP.
Let’s watch a real-time data stream using QUIC’s UDP_PAYLOAD_DATAGRAM frames. Imagine we have a simple game server sending player positions.
// Server-side sender (simplified)
func sendPlayerPositions(conn *quic.Connection) {
for {
playerPos := getPlayerPosition() // Simulate getting new position data
payload, err := json.Marshal(playerPos)
if err != nil {
log.Printf("Error marshalling position: %v", err)
continue
}
// Send as an unreliable datagram
err = conn.WriteDatagram(payload)
if err != nil {
log.Printf("Error sending datagram: %v", err)
// In a real app, you'd handle connection closure or retries here
}
time.Sleep(50 * time.Millisecond) // Send 20 updates per second
}
}
// Client-side receiver (simplified)
func receivePlayerPositions(conn *quic.Connection) {
for {
buffer := make([]byte, 1500) // Max UDP payload size
n, err := conn.ReadDatagram(buffer)
if err != nil {
log.Printf("Error reading datagram: %v", err)
// Handle connection closure
return
}
var playerPos PlayerPosition
err = json.Unmarshal(buffer[:n], &playerPos)
if err != nil {
log.Printf("Error unmarshalling position: %v", err)
continue // Malformed packet, skip
}
updatePlayerInGame(playerPos) // Render the position
}
}
Here, conn.WriteDatagram(payload) sends the player’s position data. This data is not guaranteed to arrive, and it’s not guaranteed to arrive in order. However, for something like player positions in a game, this is exactly what we want. If a packet containing an old position is lost or delayed, it doesn’t matter; we only care about the latest, most up-to-date position. This is a stark contrast to TCP, where a lost packet would halt the entire stream until it’s retransmitted, causing noticeable lag.
The problem QUIC’s unreliable datagrams solve is the Head-of-Line (HOL) blocking problem inherent in TCP. In TCP, if packet 5 is lost, packets 6, 7, and 8 must wait in the receiver’s buffer until packet 5 is successfully retransmitted and delivered. This is fine for web pages or file transfers, but for real-time applications like VoIP, video conferencing, or online gaming, this delay is unacceptable. QUIC, by separating the reliable streams from unreliable datagrams, allows these datagrams to be processed as they arrive, regardless of the status of other, reliable streams on the same connection.
The UDP_PAYLOAD_DATAGRAM frame is the core mechanism. When conn.WriteDatagram is called, the QUIC implementation packages the provided payload into a UDP_PAYLOAD_DATAGRAM frame. This frame is then sent over the underlying UDP connection. Crucially, this frame is not subject to the same retransmission and ordering guarantees as frames belonging to QUIC’s reliable streams (like STREAM frames). The QUIC receiver, upon encountering a UDP_PAYLOAD_DATAGRAM frame, immediately passes the payload to the application. There’s no waiting for acknowledgments or for preceding frames to arrive.
The key levers you control are minimal. You decide what data to send as a datagram and when. The quic-go library (or any other QUIC implementation) handles the framing and sending. The underlying UDP transport handles the actual network delivery, with all its inherent unreliability. The critical decision is whether the data you’re sending can tolerate packet loss and out-of-order delivery. If it can, and if timeliness is paramount, unreliable datagrams are your friend.
The most surprising thing about QUIC’s unreliable datagrams is that they can, in practice, offer better perceived reliability for real-time data than TCP’s guaranteed delivery. TCP’s guarantees are a double-edged sword: they ensure every byte arrives in order, but at the cost of introducing latency when any packet is lost or delayed, directly impacting time-sensitive applications. QUIC’s datagrams embrace the network’s inherent unreliability for these specific use cases, prioritizing freshness over absolute certainty, which is often the more desirable trade-off for real-time experiences.
When you send a datagram and the underlying UDP packet is dropped by an intermediary network device (like a router experiencing congestion), your datagram is simply gone. QUIC doesn’t magically make UDP reliable; it simply provides an option to bypass TCP’s reliability mechanisms for data that doesn’t need them.
The next thing you’ll likely explore is how to handle application-level reliability for these datagrams if you do need some form of guarantee, perhaps by building sequence numbers and acknowledgments directly into your application protocol on top of QUIC datagrams.