← Back to DevBytes

Implementing UDP Protocol: From Theory to Practice

Understanding UDP: The Lightweight Transport Protocol

User Datagram Protocol (UDP) is one of the core protocols of the Internet Protocol Suite, operating at the transport layer alongside TCP. Unlike its connection-oriented counterpart, UDP offers a minimal, message-based communication model that prioritizes speed and simplicity over guaranteed delivery. For developers building real-time systems, networked games, IoT applications, or streaming services, understanding how to implement UDP from scratch is an essential skill that opens doors to high-performance, low-latency communication patterns.

What Exactly Is UDP?

UDP is a connectionless transport protocol defined in RFC 768. It provides an unreliable datagram service — "unreliable" not as a criticism, but as a design choice. UDP does not establish a connection before sending data, does not guarantee delivery, does not preserve ordering, and does not implement flow control or congestion avoidance. What it does provide is a thin layer over IP that adds port numbers and an optional checksum, enabling applications to send discrete messages (datagrams) with minimal overhead.

Each UDP datagram is self-contained: it carries its own source and destination port, length, and payload. The protocol does not split data into segments or reassemble it — if a message is larger than the network's MTU (typically around 1500 bytes), IP fragmentation occurs, which can reduce reliability. Understanding these fundamentals is critical before diving into implementation.

UDP Header Structure

The UDP header is remarkably simple — just 8 bytes fixed overhead per datagram:

 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            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        ...Payload Data...                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

The theoretical maximum datagram size is 65,535 bytes (since the Length field is 16-bit), but in practice, applications limit payloads to much smaller sizes to avoid IP fragmentation issues.

Why UDP Matters: Real-World Use Cases

UDP's simplicity makes it ideal for scenarios where speed trumps reliability and where application-layer protocols can handle their own error recovery:

UDP Socket Programming: The Core API

Working with UDP sockets differs significantly from TCP. There is no listen/accept pattern — a single socket can send to and receive from multiple remote endpoints. The key system calls across platforms follow the same pattern: create a socket with SOCK_DGRAM, bind to a local port, and use sendto/recvfrom (or send/recv on connected UDP sockets). Let's walk through complete implementations.

Python Implementation: A Complete UDP Echo Server and Client

Python's socket module provides a clean, cross-platform interface to UDP. Here is a fully functional UDP echo server that listens for datagrams and echoes them back to the sender, followed by a client that sends messages and waits for responses:

#!/usr/bin/env python3
"""
udp_echo_server.py
A complete asynchronous UDP echo server using Python's select module
for handling multiple concurrent datagram sources without threads.
"""

import socket
import select
import sys

def run_echo_server(host='0.0.0.0', port=9999):
    # Create a UDP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # Bind to the specified address and port
    bind_address = (host, port)
    sock.bind(bind_address)
    print(f"[SERVER] Listening on {host}:{port}")
    
    # Use select to block until the socket is readable
    while True:
        readable, _, _ = select.select([sock], [], [])
        for s in readable:
            if s == sock:
                try:
                    # recvfrom returns (data, address) tuple
                    # Buffer size 4096 is typical for UDP messages
                    data, client_addr = sock.recvfrom(4096)
                    print(f"[SERVER] Received {len(data)} bytes from {client_addr}: "
                          f"{data.decode('utf-8', errors='replace')}")
                    
                    # Echo the data back to the client
                    bytes_sent = sock.sendto(data, client_addr)
                    print(f"[SERVER] Echoed {bytes_sent} bytes back to {client_addr}")
                except OSError as e:
                    print(f"[SERVER] Error: {e}")
                    continue

if __name__ == '__main__':
    try:
        run_echo_server()
    except KeyboardInterrupt:
        print("\n[SERVER] Shutting down gracefully...")
        sys.exit(0)
#!/usr/bin/env python3
"""
udp_echo_client.py
A UDP echo client that sends messages and waits for echoed responses.
Includes timeout handling to demonstrate unreliability awareness.
"""

import socket
import sys
import time

def send_udp_message(server_host='127.0.0.1', server_port=9999, message='Hello UDP!'):
    # Create UDP socket - no bind needed for client (OS assigns ephemeral port)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    # Set a timeout to avoid blocking forever if response is lost
    sock.settimeout(3.0)  # 3 seconds
    
    server_address = (server_host, server_port)
    
    try:
        # Encode string to bytes and send
        payload = message.encode('utf-8')
        print(f"[CLIENT] Sending {len(payload)} bytes to {server_address}")
        sock.sendto(payload, server_address)
        
        # Wait for echo response
        try:
            data, addr = sock.recvfrom(4096)
            print(f"[CLIENT] Received {len(data)} bytes from {addr}: "
                  f"{data.decode('utf-8', errors='replace')}")
            # Verify the response came from our intended server
            if addr == server_address:
                print("[CLIENT] Response verified from correct server")
            else:
                print(f"[CLIENT] WARNING: Response from unexpected address {addr}")
        except socket.timeout:
            print("[CLIENT] TIMEOUT: No response received within 3 seconds")
            print("[CLIENT] This demonstrates UDP unreliability - packet may have been lost")
    
    finally:
        sock.close()

def demonstrate_unreliability():
    """Send multiple messages to show packet loss possibility"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(1.0)
    server = ('127.0.0.1', 9999)
    
    for i in range(10):
        message = f"Packet {i} - {time.time()}"
        sock.sendto(message.encode('utf-8'), server)
        try:
            data, _ = sock.recvfrom(4096)
            print(f"Packet {i}: Response received ✓")
        except socket.timeout:
            print(f"Packet {i}: Response LOST ✗ (this is normal UDP behavior)")
    
    sock.close()

if __name__ == '__main__':
    if len(sys.argv) > 1:
        send_udp_message(sys.argv[1], 9999, ' '.join(sys.argv[2:]))
    else:
        send_udp_message()
        print("\n--- Demonstrating potential packet loss ---\n")
        demonstrate_unreliability()

Node.js Implementation: UDP with dgram Module

Node.js provides first-class UDP support through its built-in dgram module. Here's a complete UDP server and client that demonstrate both unicast and multicast patterns:

// udp_server.js
// A Node.js UDP server demonstrating message handling and reply semantics

const dgram = require('dgram');

// Create a UDP socket
const server = dgram.createSocket('udp4');

// Track client statistics
const clientStats = new Map();

server.on('message', (msg, rinfo) => {
    const clientKey = `${rinfo.address}:${rinfo.port}`;
    
    // Update stats for this client
    if (!clientStats.has(clientKey)) {
        clientStats.set(clientKey, { count: 0, lastSeen: Date.now() });
    }
    const stats = clientStats.get(clientKey);
    stats.count += 1;
    stats.lastSeen = Date.now();
    
    console.log(`[SERVER] Received ${msg.length} bytes from ${clientKey}: "${msg.toString()}"`);
    console.log(`[SERVER] Client ${clientKey} has sent ${stats.count} messages total`);
    
    // Craft a response with metadata
    const response = JSON.stringify({
        echo: msg.toString(),
        received_at: new Date().toISOString(),
        your_message_count: stats.count,
        server_uptime_ms: Math.floor(process.uptime() * 1000)
    });
    
    // Send response back to the originating client
    server.send(Buffer.from(response), rinfo.port, rinfo.address, (err) => {
        if (err) {
            console.error(`[SERVER] Failed to send response: ${err.message}`);
        } else {
            console.log(`[SERVER] Response sent to ${clientKey}`);
        }
    });
});

server.on('listening', () => {
    const address = server.address();
    console.log(`[SERVER] UDP server listening on ${address.address}:${address.port}`);
});

server.on('error', (err) => {
    console.error(`[SERVER] Fatal error: ${err.stack}`);
    server.close();
    process.exit(1);
});

// Bind to port
server.bind(41234, '0.0.0.0');
// udp_client.js
// A Node.js UDP client with exponential backoff retry logic

const dgram = require('dgram');

function createUdpClient(host, port) {
    const client = dgram.createSocket('udp4');
    const pendingRequests = new Map(); // messageId -> { resolve, reject, timer }
    let messageCounter = 0;
    
    client.on('message', (msg, rinfo) => {
        // Parse response and match to pending request
        try {
            const response = JSON.parse(msg.toString());
            // Find and resolve the pending promise
            for (const [id, pending] of pendingRequests) {
                clearTimeout(pending.timer);
                pending.resolve({
                    data: response,
                    from: rinfo,
                    latency: Date.now() - pending.sentAt
                });
                pendingRequests.delete(id);
                break; // One response per request in this simple demo
            }
        } catch (e) {
            console.log(`[CLIENT] Unmatched response: ${msg.toString()}`);
        }
    });
    
    return {
        send: (message, timeout = 2000, maxRetries = 3) => {
            return new Promise((resolve, reject) => {
                const id = ++messageCounter;
                let retries = 0;
                
                function attempt() {
                    const payload = Buffer.from(message);
                    client.send(payload, 0, payload.length, port, host, (err) => {
                        if (err) {
                            reject(err);
                            return;
                        }
                        console.log(`[CLIENT] Sent message id=${id}, attempt=${retries + 1}`);
                    });
                    
                    const timer = setTimeout(() => {
                        retries++;
                        if (retries < maxRetries) {
                            console.log(`[CLIENT] Timeout, retrying (${retries}/${maxRetries})...`);
                            attempt();
                        } else {
                            pendingRequests.delete(id);
                            reject(new Error(`Request timed out after ${maxRetries} retries`));
                        }
                    }, timeout * (retries + 1)); // Exponential-ish backoff
                    
                    pendingRequests.set(id, { resolve, reject, timer, sentAt: Date.now() });
                }
                
                attempt();
            });
        },
        close: () => client.close()
    };
}

// Usage demonstration
(async () => {
    const client = createUdpClient('127.0.0.1', 41234);
    
    try {
        const result = await client.send('Hello from Node.js UDP client!');
        console.log(`[CLIENT] Response: ${JSON.stringify(result.data, null, 2)}`);
        console.log(`[CLIENT] Latency: ${result.latency}ms`);
    } catch (err) {
        console.error(`[CLIENT] Failed: ${err.message}`);
    }
    
    client.close();
})();

C Implementation: Low-Level UDP with Raw Sockets

For systems programming or embedded contexts, working directly with the Berkeley socket API in C gives maximum control. Here is a complete, well-documented UDP server and client in C:

/*
 * udp_server.c
 * Complete UDP server in C using POSIX sockets
 * Compile: gcc -Wall -Wextra -o udp_server udp_server.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#define BUFFER_SIZE 65536  /* Max theoretical UDP datagram size */
#define PORT 9999

static volatile int keep_running = 1;

void signal_handler(int sig) {
    if (sig == SIGINT || sig == SIGTERM) {
        keep_running = 0;
    }
}

int main(void) {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];
    ssize_t recv_len, send_len;
    
    /* Register signal handler for graceful shutdown */
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    
    /* Create UDP socket (SOCK_DGRAM) */
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    
    /* Allow reuse of address to avoid TIME_WAIT issues on restart */
    int reuse = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
        perror("setsockopt SO_REUSEADDR failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    
    /* Configure server address structure */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;          /* IPv4 */
    server_addr.sin_addr.s_addr = INADDR_ANY;  /* Listen on all interfaces */
    server_addr.sin_port = htons(PORT);        /* Convert port to network byte order */
    
    /* Bind socket to address */
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    
    printf("[SERVER] Listening on 0.0.0.0:%d (UDP)\n", PORT);
    printf("[SERVER] Buffer size: %d bytes\n", BUFFER_SIZE);
    
    /* Main receive loop */
    while (keep_running) {
        memset(buffer, 0, BUFFER_SIZE);
        memset(&client_addr, 0, sizeof(client_addr));
        client_addr_len = sizeof(client_addr);
        
        /* recvfrom blocks until a datagram arrives */
        recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                           (struct sockaddr *)&client_addr, &client_addr_len);
        
        if (recv_len == -1) {
            if (errno == EINTR) {
                /* Interrupted by signal - check keep_running flag */
                continue;
            }
            perror("recvfrom failed");
            break;
        }
        
        /* Ensure null termination for string printing */
        buffer[recv_len] = '\0';
        
        /* Extract client IP and port for logging */
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
        int client_port = ntohs(client_addr.sin_port);
        
        printf("[SERVER] Received %zd bytes from %s:%d\n",
               recv_len, client_ip, client_port);
        printf("[SERVER] Payload: %s\n", buffer);
        
        /* Echo the data back to the client */
        send_len = sendto(sockfd, buffer, recv_len, 0,
                         (struct sockaddr *)&client_addr, client_addr_len);
        
        if (send_len == -1) {
            perror("sendto failed");
        } else {
            printf("[SERVER] Echoed %zd bytes back to client\n", send_len);
        }
    }
    
    printf("[SERVER] Shutting down gracefully...\n");
    close(sockfd);
    return EXIT_SUCCESS;
}
/*
 * udp_client.c
 * Complete UDP client in C
 * Compile: gcc -Wall -Wextra -o udp_client udp_client.c
 * Usage: ./udp_client [server_ip] [port] [message]
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>

#define BUFFER_SIZE 4096
#define DEFAULT_TIMEOUT_SEC 3

int main(int argc, char *argv[]) {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    const char *server_ip = (argc > 1) ? argv[1] : "127.0.0.1";
    int server_port = (argc > 2) ? atoi(argv[2]) : 9999;
    const char *message = (argc > 3) ? argv[3] : "Hello from C UDP client!";
    ssize_t msg_len, recv_len;
    
    /* Create UDP socket */
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    
    /* Set receive timeout */
    struct timeval tv;
    tv.tv_sec = DEFAULT_TIMEOUT_SEC;
    tv.tv_usec = 0;
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) {
        perror("setsockopt SO_RCVTIMEO failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    
    /* Configure server address */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    
    if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) != 1) {
        fprintf(stderr, "Invalid IP address: %s\n", server_ip);
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    
    /* Send datagram */
    msg_len = strlen(message);
    printf("[CLIENT] Sending %zd bytes to %s:%d\n", msg_len, server_ip, server_port);
    
    ssize_t sent = sendto(sockfd, message, msg_len, 0,
                         (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (sent == -1) {
        perror("sendto failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("[CLIENT] Sent %zd bytes\n", sent);
    
    /* Wait for response */
    memset(buffer, 0, BUFFER_SIZE);
    struct sockaddr_in response_addr;
    socklen_t response_addr_len = sizeof(response_addr);
    
    recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                       (struct sockaddr *)&response_addr, &response_addr_len);
    
    if (recv_len == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("[CLIENT] TIMEOUT: No response within %d seconds\n", DEFAULT_TIMEOUT_SEC);
            printf("[CLIENT] This illustrates UDP's unreliable nature\n");
        } else {
            perror("recvfrom failed");
        }
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    
    buffer[recv_len] = '\0';
    
    char response_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &response_addr.sin_addr, response_ip, INET_ADDRSTRLEN);
    
    printf("[CLIENT] Received %zd bytes from %s:%d\n",
           recv_len, response_ip, ntohs(response_addr.sin_port));
    printf("[CLIENT] Response: %s\n", buffer);
    
    close(sockfd);
    return EXIT_SUCCESS;
}

Handling UDP's Limitations: Building Reliability

Since UDP offers no guarantees, production systems often implement application-layer reliability mechanisms. Here are the key patterns:

1. Sequence Numbers and Acknowledgments

Assign monotonically increasing sequence numbers to each datagram. The receiver tracks expected sequences and can detect gaps. Critical messages require explicit acknowledgment from the receiver.

#!/usr/bin/env python3
"""
reliable_udp_layer.py
Demonstrates a minimal reliability layer over UDP using
sequence numbers, acknowledgments, and retransmission.
"""

import socket
import struct
import time
import threading
from collections import defaultdict
from dataclasses import dataclass
from enum import IntEnum

class PacketType(IntEnum):
    DATA = 0
    ACK = 1
    HEARTBEAT = 2

@dataclass
class Packet:
    seq_num: int
    packet_type: PacketType
    payload: bytes
    
    HEADER_FORMAT = "!II"  # seq_num (4 bytes), type (4 bytes)
    HEADER_SIZE = 8
    
    def serialize(self) -> bytes:
        header = struct.pack(self.HEADER_FORMAT, self.seq_num, self.packet_type.value)
        return header + self.payload
    
    @classmethod
    def deserialize(cls, data: bytes):
        if len(data) < cls.HEADER_SIZE:
            return None
        seq_num, ptype = struct.unpack(cls.HEADER_FORMAT, data[:cls.HEADER_SIZE])
        return cls(seq_num=seq_num, packet_type=PacketType(ptype), 
                   payload=data[cls.HEADER_SIZE:])

class ReliableUdpEndpoint:
    def __init__(self, sock, remote_addr, timeout=1.0, max_retries=5):
        self.sock = sock
        self.remote_addr = remote_addr
        self.timeout = timeout
        self.max_retries = max_retries
        self.seq_num = 0
        self.expected_seq = 0
        self.pending_acks = defaultdict(int)  # seq_num -> retry_count
        self.received_buffer = {}  # seq_num -> Packet
        self._lock = threading.Lock()
        self._running = True
    
    def send_reliable(self, payload: bytes) -> bool:
        """Send data and wait for acknowledgment with retransmission"""
        with self._lock:
            self.seq_num += 1
            packet = Packet(seq_num=self.seq_num, 
                           packet_type=PacketType.DATA,
                           payload=payload)
        
        serialized = packet.serialize()
        retries = 0
        
        while retries < self.max_retries:
            self.sock.sendto(serialized, self.remote_addr)
            
            # Wait for ACK
            try:
                self.sock.settimeout(self.timeout * (retries + 1))
                data, _ = self.sock.recvfrom(4096)
                ack = Packet.deserialize(data)
                if ack and ack.packet_type == PacketType.ACK and ack.seq_num == packet.seq_num:
                    return True
            except socket.timeout:
                retries += 1
                print(f"[RELIABLE] Retry {retries}/{self.max_retries} for seq={packet.seq_num}")
        
        return False
    
    def receive_loop(self):
        """Background thread that processes incoming packets and sends ACKs"""
        while self._running:
            try:
                self.sock.settimeout(0.5)
                data, addr = self.sock.recvfrom(65536)
                packet = Packet.deserialize(data)
                if packet is None:
                    continue
                
                if packet.packet_type == PacketType.DATA:
                    # Send acknowledgment immediately
                    ack = Packet(seq_num=packet.seq_num,
                                packet_type=PacketType.ACK,
                                payload=b'')
                    self.sock.sendto(ack.serialize(), addr)
                    
                    # Store in-order, handle out-of-order
                    with self._lock:
                        self.received_buffer[packet.seq_num] = packet
                        # Deliver in-order packets to application
                        while self.expected_seq + 1 in self.received_buffer:
                            self.expected_seq += 1
                            deliver = self.received_buffer.pop(self.expected_seq)
                            print(f"[RELIABLE] Delivered seq={deliver.seq_num}: "
                                  f"{deliver.payload.decode()}")
                
                elif packet.packet_type == PacketType.ACK:
                    print(f"[RELIABLE] Received ACK for seq={packet.seq_num}")
                    
            except socket.timeout:
                continue
            except Exception as e:
                print(f"[RELIABLE] Error in receive loop: {e}")

# Usage demonstration
def demo_reliable_udp():
    # Server side
    server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_sock.bind(('127.0.0.1', 9998))
    
    # Client side
    client_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    client = ReliableUdpEndpoint(client_sock, ('127.0.0.1', 9998))
    
    # Start receive thread
    receiver = threading.Thread(target=client.receive_loop, daemon=True)
    receiver.start()
    
    # Send reliable messages
    for i in range(5):
        success = client.send_reliable(f"Critical message {i}".encode())
        print(f"Message {i} delivered reliably: {success}")
        time.sleep(0.1)
    
    client_sock.close()
    server_sock.close()

if __name__ == '__main__':
    demo_reliable_udp()

2. Forward Error Correction (FEC)

For real-time applications where retransmission latency is unacceptable, FEC encodes redundant data across multiple packets so the receiver can reconstruct lost packets without requesting retransmission. Libraries like libfec or custom XOR-based schemes are common.

3. Rate Limiting and Congestion Control

Unlike TCP, UDP applications must implement their own congestion control to avoid overwhelming the network. Techniques include token bucket filters, leaky bucket algorithms, and adaptive bitrate adjustment based on observed packet loss.

#!/usr/bin/env python3
"""
token_bucket_rate_limiter.py
Token bucket implementation for UDP rate limiting
"""

import time
import threading

class TokenBucket:
    def __init__(self, rate_bytes_per_sec, burst_size_bytes):
        self.rate = rate_bytes_per_sec       # Sustained rate
        self.burst = burst_size_bytes        # Max instantaneous burst
        self.tokens = float(burst_size_bytes)  # Current token count
        self.last_refill = time.monotonic()
        self._lock = threading.Lock()
    
    def _refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        self.tokens = min(self.burst, 
                         self.tokens + elapsed * self.rate)
        self.last_refill = now
    
    def consume(self, bytes_count: int) -> bool:
        """Return True if enough tokens available, False if rate limited"""
        with self._lock:
            self._refill()
            if self.tokens >= bytes_count:
                self.tokens -= bytes_count
                return True
            return False
    
    def wait_for_tokens(self, bytes_count: int, max_wait: float = None):
        """Block until enough tokens are available"""
        deadline = time.monotonic() + max_wait if max_wait else None
        while True:
            if self.consume(bytes_count):
                return True
            if deadline and time.monotonic() > deadline:
                return False
            # Sleep for the approximate time needed to accumulate tokens
            needed = bytes_count - self.tokens
            sleep_time = needed / self.rate
            time.sleep(min(sleep_time, 0.050))  # Cap at 50ms granularity

# Usage with UDP
def rate_limited_send(sock, data, addr, bucket: TokenBucket):
    if bucket.consume(len(data)):
        sock.sendto(data, addr)
        return True
    else:
        print(f"[RATE-LIMIT] Dropping {len(data)} byte packet (rate exceeded)")
        return False

# Example: 1 Mbps sustained, 64KB burst
bucket = TokenBucket(rate_bytes_per_sec=125_000, burst_size_bytes=65536)

Best Practices for UDP Development

Testing UDP Applications with Simulated Network Conditions

On Linux, you can simulate real-world network conditions using tc netem:

# Add 5% packet loss, 50ms latency, and 10

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles