UDP: When Speed Trumps Reliability

The Live Stream That Couldn't Wait

You're watching the World Cup final. Your team is about to score. The commentator screams—but on your screen, the ball is still mid-air. By the time you see the goal, your neighbor has already erupted in celebration. You've just experienced the pain of network latency, and it's a problem that UDP was born to solve.
In the world of real-time applications—video streaming, online gaming, VoIP calls—waiting for perfect data delivery is worse than receiving slightly imperfect data immediately. A dropped video frame is barely noticeable, but a 500ms delay makes a video call unbearable. This is where UDP shines.
Let's understand why UDP exists, when to use it, and how it works under the hood.

TCP's Reliability Tax

What Problem Does UDP Solve?

The core challenge: Not all applications need TCP's guarantees. When you send data over a network, TCP provides:
  1. Guaranteed delivery: Packets are retransmitted if lost
  2. Ordered delivery: Packets arrive in the exact order sent
  3. Connection state: Three-way handshake before data transfer
  4. Flow control: Prevents overwhelming the receiver
  5. Congestion control: Adapts to network conditions
But these guarantees come at a cost:
Head-of-line blocking: If packet #5 is lost, packets #6-10 must wait for retransmission
Connection overhead: Three-way handshake adds latency (1.5 RTT minimum)
Retransmission delays: Lost packets cause backoff and retransmission
State management: Both ends maintain connection state
Why was UDP needed?
For real-time applications, these TCP features become bugs:
  • A lost video frame from 100ms ago is useless—don't retransmit it
  • Voice packets out of order are better than delayed packets
  • Gaming input needs to arrive NOW, not reliably in 200ms
Flow diagram showing process

Flow diagram showing process

Real-World Analogy

Think of UDP vs TCP like postcards vs certified mail:
TCP (Certified Mail):
  • Requires signature confirmation
  • Tracked at every step
  • Guaranteed delivery or money back
  • Takes longer, costs more
  • Perfect for legal documents
UDP (Postcard):
  • Drop in mailbox and forget
  • Fast delivery
  • No confirmation needed
  • Might get lost (rarely)
  • Perfect for holiday greetings
Just like you wouldn't send a birthday card via certified mail, you wouldn't stream a video game over TCP.

UDP's Minimalist Approach

How Does UDP Solve the Problem?

UDP takes a radical approach: strip away all the reliability guarantees and keep only what's absolutely necessary.
UDP provides only:
  1. Port numbers: Multiplex multiple applications on one IP
  2. Checksum: Detect corrupted packets (optional!)
  3. Length field: Know where the packet ends
That's it. No handshakes, no retransmission, no ordering, no connection state.
Why this approach works:
Zero connection setup: Start sending immediately
No head-of-line blocking: Each packet is independent
Minimal overhead: Only 8 bytes of header (vs TCP's 20+ bytes)
Application control: You decide how to handle losses
Multicast support: Send one packet to many receivers
Flow diagram showing process

Flow diagram showing process

Key innovation: Simplicity through omission. By removing TCP's features, UDP enables use cases TCP can't support.

Understanding UDP's Flow

The UDP Communication Lifecycle

Let's build a complete mental model of how UDP works from application to network.
No Connection Setup (Unlike TCP)

No Connection Setup (Unlike TCP)

Key observations:
  1. No handshake: UDP starts sending immediately
  2. Fire and forget: Sender doesn't wait for acknowledgment
  3. Stateless: No connection state to maintain
  4. Application responsibility: App must handle losses, ordering, duplicates

UDP Packet Structure

Understanding the packet structure reveals why UDP is so fast:
Diagram 4

Diagram 4

Compare with TCP:
  • UDP header: 8 bytes
  • TCP header: 20-60 bytes (5-15x larger!)
This minimal header means:
  • Less bandwidth waste
  • Faster processing
  • More payload per packet

State Diagram: UDP Connection Lifecycle

UDP doesn't really have "connections," but here's the state model:
Flow diagram showing process

Flow diagram showing process

Mental model: UDP sockets are like mailboxes:
  • You can receive mail from anyone
  • You can send mail to anyone
  • No "connection" required
  • Just drop your message and go

Deep Technical Dive

UDP Header Breakdown

Let's examine every field and understand WHY it exists:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Length | Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | | ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Field-by-field breakdown:
  1. Source Port (16 bits)
    • Identifies sending application
    • Range: 0-65,535
    • Why it matters: Allows multiplexing multiple apps on one IP
    • Can be 0 if sender doesn't need replies
  2. Destination Port (16 bits)
    • Identifies receiving application
    • Why it matters: Directs packet to correct app
    • Well-known ports: DNS (53), DHCP (67/68), NTP (123)
  3. Length (16 bits)
    • Total length: header + data
    • Minimum: 8 bytes (header only)
    • Maximum: 65,535 bytes
    • Why it matters: Receiver knows where packet ends
  4. Checksum (16 bits)
    • Detects corruption in header + data
    • Optional in IPv4 (can be 0)
    • Mandatory in IPv6
    • Why it matters: Prevents delivering corrupted data
Checksum calculation:
1. Compute 16-bit one's complement sum of: - Pseudo IP header (source IP, dest IP, protocol, UDP length) - UDP header - UDP data (padded to 16-bit boundary) 2. Take one's complement of the result 3. If result is 0, use 0xFFFF

Code Deep Dive: Building a UDP Client-Server

Let's implement a complete UDP echo server and client in Go to understand the mechanics.

Example 1: UDP Echo Server

go
package main import ( "fmt" "net" "os" ) func main() { // Create UDP address // Why UDP? We want fast, connectionless communication serverAddr, err := net.ResolveUDPAddr("udp", ":8080") if err != nil { fmt.Println("Error resolving address:", err) os.Exit(1) } // Create UDP socket and bind to port // Unlike TCP, this doesn't "listen" - it just opens a mailbox conn, err := net.ListenUDP("udp", serverAddr) if err != nil { fmt.Println("Error listening:", err) os.Exit(1) } defer conn.Close() fmt.Println("UDP Server listening on :8080") // Buffer for incoming packets // Why 1024? Common MTU is 1500 bytes, safe size for most packets buffer := make([]byte, 1024) for { // Read from UDP socket // This blocks until a packet arrives // No connection needed - we receive from anyone! n, clientAddr, err := conn.ReadFromUDP(buffer) if err != nil { fmt.Println("Error reading:", err) continue } // Log what we received message := string(buffer[:n]) fmt.Printf("Received %d bytes from %s: %s\n", n, clientAddr.String(), message) // Echo back to sender // No connection state - just send to the address we got _, err = conn.WriteToUDP(buffer[:n], clientAddr) if err != nil { fmt.Println("Error writing:", err) continue } fmt.Printf("Echoed back to %s\n", clientAddr.String()) } }
What happens under the hood:
  1. ListenUDP() creates a UDP socket and binds to port 8080
    • Kernel allocates a socket buffer
    • No SYN/ACK handshake like TCP
    • Socket is immediately ready to receive
  2. ReadFromUDP() blocks waiting for a packet
    • Packet arrives at network interface
    • IP layer verifies IP checksum, routes to UDP
    • UDP verifies checksum (if non-zero)
    • Packet queued in socket receive buffer
    • Application woken up with data + sender address
  3. WriteToUDP() sends response
    • UDP adds 8-byte header
    • Passes to IP layer with destination address
    • No ACK expected, no retransmission logic

Example 2: UDP Client

go
package main import ( "bufio" "fmt" "net" "os" "time" ) func main() { // Resolve server address serverAddr, err := net.ResolveUDPAddr("udp", "localhost:8080") if err != nil { fmt.Println("Error resolving address:", err) os.Exit(1) } // Create UDP connection // "Connection" is a misnomer - UDP is connectionless // This just associates the socket with a remote address conn, err := net.DialUDP("udp", nil, serverAddr) if err != nil { fmt.Println("Error dialing:", err) os.Exit(1) } defer conn.Close() fmt.Println("Connected to UDP server at localhost:8080") fmt.Println("Type messages to send (Ctrl+C to exit):") // Read user input scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { message := scanner.Text() // Send to server // No handshake, no ACK, just fire and forget start := time.Now() _, err := conn.Write([]byte(message)) if err != nil { fmt.Println("Error sending:", err) continue } fmt.Printf("Sent: %s\n", message) // Set read deadline to timeout if server doesn't respond // Why? UDP doesn't know if packet was lost - we must handle it conn.SetReadDeadline(time.Now().Add(2 * time.Second)) // Wait for response buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { // This could be timeout or actual error if netErr, ok := err.(net.Error); ok && netErr.Timeout() { fmt.Println("Timeout - packet may have been lost") } else { fmt.Println("Error reading:", err) } continue } elapsed := time.Since(start) // Display response response := string(buffer[:n]) fmt.Printf("Received: %s (RTT: %v)\n", response, elapsed) } }
Testing the implementation:
bash
# Terminal 1: Start server go run udp_server.go # Output: UDP Server listening on :8080 # Terminal 2: Start client go run udp_client.go # Type: Hello UDP! # Output: # Sent: Hello UDP! # Received: Hello UDP! (RTT: 423µs)

Example 3: UDP Packet Loss Simulation

Let's simulate packet loss to understand UDP's behavior:
go
package main import ( "fmt" "math/rand" "net" "time" ) // Simulates unreliable network with packet loss type UnreliableUDP struct { conn *net.UDPConn lossRate float64 // 0.0 to 1.0 (0% to 100%) delayMs int // Artificial delay in ms rand *rand.Rand } func NewUnreliableUDP(conn *net.UDPConn, lossRate float64, delayMs int) *UnreliableUDP { return &UnreliableUDP{ conn: conn, lossRate: lossRate, delayMs: delayMs, rand: rand.New(rand.NewSource(time.Now().UnixNano())), } } func (u *UnreliableUDP) WriteToUDP(data []byte, addr *net.UDPAddr) (int, error) { // Simulate packet loss // This is what happens in real networks! if u.rand.Float64() < u.lossRate { fmt.Printf("[SIMULATED LOSS] Dropped packet to %s\n", addr.String()) // Pretend we sent it (app doesn't know it was lost) return len(data), nil } // Simulate network delay if u.delayMs > 0 { time.Sleep(time.Duration(u.delayMs) * time.Millisecond) } return u.conn.WriteToUDP(data, addr) } func main() { serverAddr, _ := net.ResolveUDPAddr("udp", ":8080") conn, _ := net.ListenUDP("udp", serverAddr) defer conn.Close() // Create unreliable UDP with 30% loss rate and 50ms delay // This simulates a poor quality network unreliable := NewUnreliableUDP(conn, 0.3, 50) fmt.Println("Unreliable UDP Server (30% loss, 50ms delay)") buffer := make([]byte, 1024) for { n, clientAddr, err := conn.ReadFromUDP(buffer) if err != nil { continue } message := string(buffer[:n]) fmt.Printf("Received: %s from %s\n", message, clientAddr) // Try to echo back (but might be lost!) _, err = unreliable.WriteToUDP(buffer[:n], clientAddr) if err != nil { fmt.Println("Error:", err) } } }
Output example:
Unreliable UDP Server (30% loss, 50ms delay) Received: packet 1 from 127.0.0.1:54321 Received: packet 2 from 127.0.0.1:54321 [SIMULATED LOSS] Dropped packet to 127.0.0.1:54321 Received: packet 3 from 127.0.0.1:54321 Received: packet 4 from 127.0.0.1:54321 [SIMULATED LOSS] Dropped packet to 127.0.0.1:54321
This demonstrates:
  • UDP provides no retransmission
  • Packets can be lost silently
  • Application must detect and handle losses
  • This is exactly what happens in real networks!

Benefits & Why UDP Matters

Performance Benefits

1. Zero Connection Overhead
Diagram 6

Diagram 6

Impact: UDP can start sending data immediately, saving 1.5 RTT (Round Trip Time).
  • On a 100ms RTT network: TCP wastes 150ms before sending data
  • For DNS query: UDP responds in ~50ms, TCP would need 200ms+
  • For gaming: Every millisecond matters for responsiveness
2. No Head-of-Line Blocking
TCP scenario:
Packets sent: 1, 2, 3, 4, 5 Packet 3 lost Packets 4 and 5 must wait for packet 3 to be retransmitted Total delay: RTT + retransmission time
UDP scenario:
Packets sent: 1, 2, 3, 4, 5 Packet 3 lost Packets 4 and 5 delivered immediately Total delay: 0 (app handles the gap)
Real-world impact:
  • Video streaming: Drop one frame, show the next (60fps = 16ms per frame)
  • Gaming: Latest position update is what matters, not old ones
  • VoIP: Small gaps in audio are less noticeable than stuttering
3. Multicast and Broadcast Support
UDP enables one-to-many communication:
go
// Send one packet to multiple receivers func multicastExample() { // Join multicast group 239.0.0.1 addr, _ := net.ResolveUDPAddr("udp", "239.0.0.1:9999") conn, _ := net.DialUDP("udp", nil, addr) // One send reaches all subscribers! conn.Write([]byte("Hello everyone!")) }
Use cases:
  • Stock price updates to thousands of traders
  • Video streaming to multiple viewers
  • Service discovery (mDNS, SSDP)
  • Game state updates to all players

Developer Experience

Simplicity:
go
// TCP: Complex connection management listener, _ := net.Listen("tcp", ":8080") for { conn, _ := listener.Accept() // Wait for connection go handleConnection(conn) // Spawn goroutine per connection } // UDP: Simple datagram handling conn, _ := net.ListenUDP("udp", addr) for { n, addr, _ := conn.ReadFromUDP(buffer) // Read from anyone conn.WriteToUDP(buffer[:n], addr) // Reply }
No connection state:
  • No need to track connections
  • No cleanup on disconnect
  • No TIME_WAIT sockets

Real-World Success Stories

1. DNS (Domain Name System)
  • Uses UDP port 53
  • Typical query: < 50 bytes
  • Typical response: < 512 bytes
  • Adding TCP overhead would double latency
  • Result: Billions of queries/day with minimal latency
2. QUIC (HTTP/3)
  • Built on UDP to avoid TCP's limitations
  • Implements reliability in userspace
  • Enables 0-RTT connection establishment
  • Powers 75% of Chrome's HTTPS traffic
3. Video Conferencing (Zoom, Teams)
  • Audio/video over UDP (RTP/SRTP)
  • Tolerates 1-2% packet loss
  • Adaptive bitrate compensates for losses
  • TCP would cause stuttering from retransmissions
4. Gaming (Fortnite, Call of Duty)
  • Game state updates over UDP
  • 60Hz update rate (16ms per update)
  • Old updates are worthless
  • TCP's retransmission would cause lag

Trade-offs & Gotchas

When to Use UDP

Perfect for:
  1. Real-time applications where latency > reliability
    • Video/audio streaming
    • Online gaming
    • VoIP calls
    • Live sports broadcasts
  2. Small, independent requests
    • DNS queries
    • NTP time synchronization
    • DHCP configuration
    • Simple RPC calls
  3. Multicast/broadcast scenarios
    • Service discovery
    • Cluster communication
    • Live data feeds
  4. Custom reliability protocols
    • QUIC (HTTP/3)
    • Gaming protocols with custom ACKs
    • Video streaming with FEC (Forward Error Correction)

When NOT to Use UDP

Avoid UDP for:
  1. File transfers
    • Why: Need guaranteed delivery of every byte
    • Use: TCP, SFTP, rsync
  2. Financial transactions
    • Why: Cannot afford data loss
    • Use: TCP with TLS
  3. Email, messaging
    • Why: Messages must arrive in order
    • Use: TCP (SMTP, IMAP)
  4. API calls with large responses
    • Why: Fragmentation issues, no retry logic
    • Use: HTTP/1.1 (TCP) or HTTP/3 (QUIC over UDP)

Common Mistakes

Mistake 1: Not Handling Packet Loss

go
// BAD: Assumes packet always arrives func sendCommand(conn *net.UDPConn, cmd string) { conn.Write([]byte(cmd)) // What if this packet is lost? } // GOOD: Retry with timeout func sendCommandReliable(conn *net.UDPConn, cmd string) error { maxRetries := 3 timeout := 1 * time.Second for i := 0; i < maxRetries; i++ { conn.Write([]byte(cmd)) conn.SetReadDeadline(time.Now().Add(timeout)) buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err == nil { // Got response return nil } fmt.Printf("Retry %d/%d\n", i+1, maxRetries) } return fmt.Errorf("failed after %d retries", maxRetries) }
Why this happens: Developers assume network is reliable
How to avoid: Always implement timeouts and retries for critical messages

Mistake 2: Sending Large Packets

go
// BAD: Sending 10KB packet largeData := make([]byte, 10000) conn.Write(largeData) // Will fragment across multiple IP packets // GOOD: Keep packets under MTU func sendInChunks(conn *net.UDPConn, data []byte) { const MTU = 1400 // Safe size under typical 1500 MTU for i := 0; i < len(data); i += MTU { end := i + MTU if end > len(data) { end = len(data) } chunk := data[i:end] conn.Write(chunk) } }
Why this happens: Not understanding IP fragmentation
The problem:
  • IP can fragment large UDP packets
  • If ANY fragment is lost, entire packet is lost
  • Fragmentation is often blocked by firewalls
How to avoid: Keep UDP packets < 1400 bytes

Mistake 3: Not Validating Data

go
// BAD: Trusting received data buffer := make([]byte, 1024) n, _ := conn.Read(buffer) value := binary.LittleEndian.Uint32(buffer[:4]) // Could panic! // GOOD: Validate before using buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { return err } if n < 4 { return fmt.Errorf("packet too short") } value := binary.LittleEndian.Uint32(buffer[:4])
Why this happens: Assuming checksums catch all corruption
The reality:
  • Checksum is optional in IPv4
  • Even with checksum, malicious packets can be sent
  • Network errors can corrupt data in complex ways
How to avoid: Always validate packet structure and contents

Mistake 4: Ignoring Packet Reordering

go
// BAD: Assuming packets arrive in order type Message struct { SequenceNum uint32 Data []byte } var lastSeq uint32 = 0 func handlePacket(msg Message) { lastSeq++ // Assuming msg.SequenceNum == lastSeq (WRONG!) processData(msg.Data) } // GOOD: Handle out-of-order packets type ReorderBuffer struct { buffer map[uint32][]byte nextSeq uint32 } func (rb *ReorderBuffer) AddPacket(seq uint32, data []byte) [][]byte { rb.buffer[seq] = data var ready [][]byte for { if data, ok := rb.buffer[rb.nextSeq]; ok { ready = append(ready, data) delete(rb.buffer, rb.nextSeq) rb.nextSeq++ } else { break } } return ready }
Why this happens: Confusing UDP with TCP
How to avoid: Use sequence numbers and reorder buffers if order matters

Mistake 5: Not Implementing Flow Control

go
// BAD: Sending as fast as possible for i := 0; i < 1000000; i++ { conn.Write([]byte(fmt.Sprintf("packet %d", i))) // Overwhelming receiver! } // GOOD: Rate limiting func sendWithRateLimit(conn *net.UDPConn, packets [][]byte, ratePerSec int) { ticker := time.NewTicker(time.Second / time.Duration(ratePerSec)) defer ticker.Stop() for _, packet := range packets { <-ticker.C // Wait for next tick conn.Write(packet) } }
Why this happens: UDP has no built-in flow control
The problem:
  • Receiver buffer can overflow
  • Packets dropped by receiver's OS
  • Network congestion from flood
How to avoid: Implement application-level rate limiting

Mistake 6: Not Handling Duplicates

go
// BAD: Processing every packet func handlePacket(data []byte) { executeCommand(data) // Might execute twice! } // GOOD: Deduplication type DeduplicationCache struct { seen map[uint32]bool mu sync.Mutex } func (dc *DeduplicationCache) IsNew(seqNum uint32) bool { dc.mu.Lock() defer dc.mu.Unlock() if dc.seen[seqNum] { return false // Duplicate } dc.seen[seqNum] = true // Clean old entries periodically if len(dc.seen) > 10000 { dc.seen = make(map[uint32]bool) } return true }
Why this happens: Network can duplicate packets (rare but possible)
How to avoid: Use sequence numbers to detect duplicates

Performance Considerations

Bottleneck 1: Socket Buffer Overflow
go
// Check and increase socket buffer size conn.SetReadBuffer(4 * 1024 * 1024) // 4MB receive buffer conn.SetWriteBuffer(4 * 1024 * 1024) // 4MB send buffer
Why it matters: Default buffers (~200KB) can overflow under load
Bottleneck 2: System Call Overhead
go
// BAD: One syscall per packet for i := 0; i < 1000; i++ { conn.Write(packets[i]) } // GOOD: Batch with sendmmsg (Linux) // Use libraries like golang.org/x/net/ipv4
Why it matters: System calls are expensive (thousands of CPU cycles)
Bottleneck 3: GC Pressure
go
// BAD: Allocating on every read for { buffer := make([]byte, 1024) // New allocation! conn.Read(buffer) } // GOOD: Reuse buffers bufferPool := sync.Pool{ New: func() interface{} { return make([]byte, 1024) }, } for { buffer := bufferPool.Get().([]byte) conn.Read(buffer) // Process... bufferPool.Put(buffer) }

Security Considerations

1. UDP Amplification Attacks
go
// Vulnerable server func vulnerableServer() { conn, _ := net.ListenUDP("udp", addr) buffer := make([]byte, 10) for { n, clientAddr, _ := conn.ReadFromUDP(buffer) if n > 0 { // Sending large response to small request largeResponse := make([]byte, 10000) conn.WriteToUDP(largeResponse, clientAddr) // Attacker can spoof clientAddr, amplifying attack! } } } // Mitigation: Rate limiting + response size limits
2. Spoofed Source Addresses
go
// Cannot trust source IP with UDP // Implement application-level authentication type AuthenticatedPacket struct { Signature [32]byte Timestamp int64 Data []byte } func verifyPacket(packet AuthenticatedPacket, secretKey []byte) bool { // HMAC verification // Timestamp check (prevent replay) return true }
3. DDoS via UDP Flood
Mitigation strategies:
  • Rate limiting per source IP
  • Connection puzzles (proof of work)
  • Stateless cookie exchange
  • Traffic shaping

Comparison with Alternatives

FeatureUDPTCPQUICSCTP
ConnectionConnectionlessConnection-orientedConnection-orientedConnection-oriented
ReliabilityNone (app handles)Guaranteed deliveryGuaranteed deliveryGuaranteed delivery
OrderingNoneIn-orderIn-order per streamIn-order per stream
Header Size8 bytes20-60 bytes~40 bytes12+ bytes
LatencyLowestHigh (handshake)Medium (0-RTT)Medium
Flow ControlNoneYesYesYes
Congestion ControlNoneYesYesYes
MulticastYesNoNoYes
NAT TraversalEasy⚠️ HarderEasyDifficult
Use CasesStreaming, gaming, DNSWeb, file transfer, emailHTTP/3, low-latency webTelephony signaling
Decision framework:
Flow diagram showing process

Flow diagram showing process

Hands-On Example: UDP Chat Application

Let's build a simple UDP chat to demonstrate practical UDP usage:
go
// chat_server.go package main import ( "fmt" "net" "sync" ) type Client struct { addr *net.UDPAddr name string } type ChatServer struct { conn *net.UDPConn clients map[string]*Client mu sync.RWMutex } func NewChatServer(port string) (*ChatServer, error) { addr, err := net.ResolveUDPAddr("udp", ":"+port) if err != nil { return nil, err } conn, err := net.ListenUDP("udp", addr) if err != nil { return nil, err } return &ChatServer{ conn: conn, clients: make(map[string]*Client), }, nil } func (s *ChatServer) Start() { fmt.Println("Chat server started") buffer := make([]byte, 1024) for { n, clientAddr, err := s.conn.ReadFromUDP(buffer) if err != nil { continue } message := string(buffer[:n]) s.handleMessage(clientAddr, message) } } func (s *ChatServer) handleMessage(addr *net.UDPAddr, message string) { addrStr := addr.String() // Handle JOIN command if len(message) > 5 && message[:5] == "JOIN:" { name := message[5:] s.mu.Lock() s.clients[addrStr] = &Client{addr: addr, name: name} s.mu.Unlock() fmt.Printf("%s joined as %s\n", addrStr, name) s.broadcast(fmt.Sprintf("%s joined the chat", name), nil) return } // Broadcast message to all clients s.mu.RLock() client, exists := s.clients[addrStr] s.mu.RUnlock() if exists { fullMessage := fmt.Sprintf("%s: %s", client.name, message) s.broadcast(fullMessage, client) } } func (s *ChatServer) broadcast(message string, sender *Client) { s.mu.RLock() defer s.mu.RUnlock() for _, client := range s.clients { // Don't echo back to sender if sender != nil && client.addr.String() == sender.addr.String() { continue } // Fire and forget - no guarantee of delivery! s.conn.WriteToUDP([]byte(message), client.addr) } } func main() { server, err := NewChatServer("8080") if err != nil { panic(err) } server.Start() }
Testing:
bash
# Terminal 1 go run chat_server.go # Output: Chat server started # Terminal 2 echo "JOIN:Alice" | nc -u localhost 8080 echo "Hello everyone!" | nc -u localhost 8080 # Terminal 3 echo "JOIN:Bob" | nc -u localhost 8080 echo "Hi Alice!" | nc -u localhost 8080
What this demonstrates:
  • UDP for real-time messaging
  • Broadcast to multiple clients
  • No guaranteed delivery (messages might be lost)
  • Stateless server (tracks clients via address)

Interview Preparation

Question 1: Explain UDP vs TCP and when to use each

Answer:
UDP is a connectionless, unreliable transport protocol while TCP is connection-oriented and reliable.
Key differences:
AspectUDPTCP
ConnectionConnectionlessRequires handshake
ReliabilityNo guaranteesGuaranteed delivery
OrderingNo orderingIn-order delivery
SpeedFaster (no overhead)Slower (ack/retransmit)
Use casesStreaming, gaming, DNSWeb, email, file transfer
When to use UDP:
  • Real-time applications (gaming, VoIP)
  • Broadcasting (service discovery)
  • Small request-response (DNS)
  • Application implements custom reliability
When to use TCP:
  • File transfers
  • Email, messaging
  • Financial transactions
  • Any data that must arrive reliably
Why they ask: Tests understanding of protocol trade-offs and practical application design
Red flags to avoid:
  • "UDP is always faster" (not always true in all scenarios)
  • "TCP is always better because it's reliable" (misses UDP use cases)
  • Not mentioning head-of-line blocking
Pro tip: Mention QUIC as an example of building reliability on top of UDP, showing you understand modern protocol design.

Question 2: What is the UDP header structure?

Answer:
UDP header is 8 bytes with 4 fields:
|Source Port (16 bits)|Dest Port (16 bits)| |Length (16 bits) |Checksum (16 bits) |
Field details:
  1. Source Port: Sender's port (0-65535), can be 0
  2. Destination Port: Receiver's port (0-65535)
  3. Length: Total length including header + data (min 8 bytes)
  4. Checksum: Optional in IPv4, mandatory in IPv6
Why each field matters:
  • Ports enable multiplexing multiple applications
  • Length tells receiver where packet ends
  • Checksum detects corruption (though optional)
Why they ask: Tests low-level protocol knowledge
Red flags to avoid:
  • Confusing with TCP header (20+ bytes)
  • Not knowing checksum is optional in IPv4
  • Not understanding minimum packet size
Pro tip: Mention that the small header (8 bytes vs TCP's 20+) is why UDP has lower overhead and is preferred for performance-critical applications.

Question 3: How would you implement reliability on top of UDP?

Answer:
Implementing reliability on UDP requires:
  1. Sequence numbers: Track packet order
  2. Acknowledgments (ACKs): Confirm receipt
  3. Timeouts: Detect lost packets
  4. Retransmission: Resend lost packets
  5. Duplicate detection: Ignore retransmitted packets
Example implementation:
go
type ReliableUDP struct { conn *net.UDPConn pendingACKs map[uint32]chan bool sequenceNum uint32 } func (r *ReliableUDP) SendReliable(data []byte, addr *net.UDPAddr) error { seqNum := atomic.AddUint32(&r.sequenceNum, 1) packet := Packet{ SeqNum: seqNum, Data: data, } maxRetries := 3 timeout := 1 * time.Second for i := 0; i < maxRetries; i++ { r.conn.WriteToUDP(packet.Encode(), addr) // Wait for ACK with timeout select { case <-time.After(timeout): continue // Retry case <-r.pendingACKs[seqNum]: return nil // Success } } return errors.New("failed after retries") }
Why they ask: Tests ability to build custom protocols and understanding of reliability mechanisms
Red flags to avoid:
  • Saying "just use TCP" (misses the point)
  • Not mentioning timeouts
  • Ignoring duplicate packets
Pro tip: Reference QUIC as a real-world example that implements TCP-like reliability over UDP, demonstrating you know industry best practices.

Question 4: What are the security concerns with UDP?

Answer:
Main security concerns:
  1. Amplification Attacks
    • Small request triggers large response
    • Attacker spoofs source IP
    • Victim receives amplified traffic
    • Example: DNS amplification (10x-100x amplification)
  2. Source IP Spoofing
    • UDP doesn't verify source address
    • Attacker can fake sender IP
    • Used in DDoS attacks
    • Mitigation: Application-level authentication
  3. UDP Flood DDoS
    • Send massive UDP packets to target
    • Overwhelm network or application
    • No handshake means easy to generate
    • Mitigation: Rate limiting, firewalls
  4. No Encryption
    • UDP itself has no encryption
    • Data sent in plaintext
    • Mitigation: DTLS (Datagram TLS)
Mitigations:
go
// Rate limiting per IP rateLimiter := make(map[string]*time.Ticker) // Authentication type AuthPacket struct { HMAC [32]byte Timestamp int64 Data []byte } // Response size limiting if len(response) > 10*len(request) { // Potential amplification attack return }
Why they ask: Tests security awareness and production readiness
Red flags to avoid:
  • Not mentioning spoofing
  • Saying "UDP is inherently insecure" (protocol-agnostic)
  • Ignoring application-layer mitigations
Pro tip: Mention DTLS for encryption and how modern protocols like QUIC include built-in security, showing awareness of secure UDP implementations.

Question 5: How does UDP handle packet loss in practice?

Answer:
UDP doesn't handle packet loss - it's the application's responsibility.
Detection methods:
  1. Sequence numbers: Gap indicates loss
  2. Timeouts: Expected packet doesn't arrive
  3. Heartbeats: Missing heartbeats suggest loss
Handling strategies:
  1. Ignore it (streaming)
    • Lost video frames are acceptable
    • Show newer frames instead
  2. Forward Error Correction (FEC)
    • Send redundant data
    • Reconstruct lost packets from redundancy
    • Used in video conferencing
  3. Selective retransmission
    • Track lost packets
    • Request retransmission
    • Used in QUIC, gaming protocols
  4. Interpolation (gaming, VoIP)
    • Predict missing data from adjacent packets
    • Smooth out gaps
Example:
go
// FEC: Send 3 packets, any 2 can reconstruct data func sendWithFEC(data []byte, conn *net.UDPConn, addr *net.UDPAddr) { chunk1 := data[:len(data)/2] chunk2 := data[len(data)/2:] redundant := xor(chunk1, chunk2) conn.WriteToUDP(chunk1, addr) conn.WriteToUDP(chunk2, addr) conn.WriteToUDP(redundant, addr) // Receiver can reconstruct from any 2 packets }
Why they ask: Tests understanding of real-world UDP usage and error handling
Red flags to avoid:
  • "UDP always retransmits" (it doesn't)
  • Not knowing different loss-handling strategies
  • Assuming loss is always a problem
Pro tip: Mention adaptive bitrate streaming (Netflix, YouTube) as an example of handling loss by adjusting quality, showing practical knowledge.

Question 6: Explain UDP's role in QUIC/HTTP3

Answer:
QUIC (Quick UDP Internet Connections) is a transport protocol built on top of UDP, used by HTTP/3.
Why QUIC uses UDP:
  1. Avoid TCP ossification
    • Middle boxes (routers, firewalls) expect TCP behavior
    • Changing TCP is nearly impossible
    • UDP is simpler, less interfered with
  2. Userspace implementation
    • TCP is in kernel (slow to update)
    • UDP allows protocol in userspace
    • Faster iteration and improvements
  3. 0-RTT connection establishment
    • TCP requires handshake (1.5 RTT)
    • QUIC can send data immediately
    • Huge latency improvement
QUIC adds on top of UDP:
  • Reliability (ACKs, retransmission)
  • Flow control
  • Congestion control
  • Stream multiplexing (no head-of-line blocking)
  • Built-in TLS 1.3 encryption

HTTP/3 → QUIC → UDP → IP
QUIC is "TCP reinvented on UDP" with modern features.
Impact:
  • 75% of Google Chrome traffic uses HTTP/3
  • Faster page loads (especially on mobile)
  • Better performance on lossy networks
Why they ask: Tests awareness of modern protocols and ability to connect concepts
Red flags to avoid:
  • "QUIC is just HTTP/2 over UDP" (it's the transport layer)
  • Not understanding why UDP was chosen
  • Confusing QUIC with HTTP/3
Pro tip: Mention that QUIC shows UDP's flexibility - you can build any transport semantics on top of it, demonstrating deep understanding of protocol layering.

Key Takeaways

🔑 UDP trades reliability for speed - By removing TCP's guarantees (retransmission, ordering, handshakes), UDP achieves minimal latency and overhead, making it perfect for real-time applications.
🔑 Connectionless = stateless simplicity - UDP doesn't maintain connection state, enabling multicast, easier implementation, and immediate data transmission without handshakes.
🔑 Application controls reliability - UDP pushes reliability decisions to the application layer, allowing custom solutions (FEC, selective retransmission, or accepting loss) based on specific needs.
🔑 8-byte header is the secret - UDP's minimal header (vs TCP's 20-60 bytes) reduces overhead and processing time, critical for high-performance applications sending millions of small packets.
🔑 Modern protocols build on UDP - QUIC/HTTP3 demonstrate UDP's flexibility as a foundation for building custom, optimized transport protocols with exactly the features needed.

Insights & Reflection

The Philosophy of UDP

UDP embodies a fundamental principle in systems design: simple primitives enable complex solutions. By providing only the bare minimum (addressing via ports, optional corruption detection), UDP lets applications build exactly the reliability model they need.
This is the opposite of TCP's philosophy: "provide everything, applications opt out." UDP says "provide nothing, applications opt in." Neither is wrong—they serve different needs.

Connection to Broader Concepts

End-to-End Principle: UDP exemplifies the end-to-end argument in system design. Don't implement features in the middle (network layer) that only some applications need. Let applications implement what they require.
Layered Architecture: UDP shows how protocols can be layered. QUIC builds reliability on UDP. RTP builds real-time semantics on UDP. Applications compose primitives to create solutions.
Trade-off Awareness: Engineering is about trade-offs. UDP sacrifices reliability for speed. Knowing when this trade-off makes sense (gaming, streaming) vs when it doesn't (file transfer, email) is the mark of a good engineer.

Evolution and Future

UDP has remained largely unchanged since 1980, yet it's more relevant than ever:
  • 1980s: DNS, TFTP (simple protocols)
  • 1990s: Streaming media emerges
  • 2000s: VoIP, gaming become mainstream
  • 2010s: QUIC invented, HTTP/3 standardized
  • 2020s: WebRTC, cloud gaming, 5G networks
The future: As networks get faster but latency remains constrained by physics (speed of light), UDP's zero-overhead model becomes increasingly valuable. Edge computing, IoT, and real-time AI applications will drive more UDP innovation.
The deeper lesson: Sometimes the best protocol is the simplest one. UDP's longevity comes from doing one thing well: get out of the way and let applications innovate.
All Blogs
Tags:udpnetworkingquictransport-layerinterview-prep