UDP vs TCP: How Much Faster Is UDP Really?

TLDR: UDP saves time before and between messages, not per byte: no handshake before the first byte, no waiting for acknowledgments, and a lost packet does not hold back the packets after it. It does not move more data. In our localhost test, TCP sent 2 to 6 times more messages per second than UDP, and UDP lost up to 2.8% of them. The UDP header is 8 bytes and the TCP header 20 bytes (32 with timestamps), so headers are rarely the difference. Choose UDP for traffic where a late packet is worth less than a lost one, such as calls, games and DNS, and TCP or QUIC for everything that must arrive complete.
When you're choosing between UDP and TCP for your application, the question "how much faster is UDP?" comes up constantly. The answer isn't straightforward because it depends heavily on your network conditions, message size, and what reliability features you need to implement on top of UDP.
What Makes UDP Faster
UDP (User Datagram Protocol) is connectionless and stateless. When you send a UDP packet, it goes straight out without handshakes, acknowledgments, or retransmission logic. Here's what UDP skips:
TCP Connection Flow:
Client Server
| |
|----SYN---------------> | (1 RTT)
|<---SYN-ACK-----------| |
|----ACK---------------> |
|----Data--------------> | (+ data transfer)
|<---ACK----------------| |
UDP Flow:
Client Server
| |
|----Data--------------> | (immediate)
|----Data--------------> |
TCP requires a three-way handshake before any data moves. That's one full round-trip time (RTT) of overhead before your first byte arrives. For a local network where RTT might be 1ms, this is negligible. For a connection across continents where RTT might be 200ms, that's a noticeable delay.
Header Overhead Comparison
The header size difference is often cited but is rarely the bottleneck:
# UDP header structure (8 bytes total)
class UDPHeader:
source_port # 2 bytes
dest_port # 2 bytes
length # 2 bytes
checksum # 2 bytes
# TCP header structure (minimum 20 bytes, typically 32+ with options)
class TCPHeader:
source_port # 2 bytes
dest_port # 2 bytes
sequence_num # 4 bytes
ack_num # 4 bytes
flags # 2 bytes
window_size # 2 bytes
checksum # 2 bytes
urgent_pointer # 2 bytes
options # 0-40 bytes (often 12 bytes for timestamps)
For a 1500-byte packet, TCP uses about 32 bytes (2.1%) while UDP uses 8 bytes (0.5%). This difference is minimal. For tiny packets like gaming updates or DNS queries, the percentage is higher but still not the main factor.
Real Performance Numbers
To see what the protocols cost on one machine, we sent 100,000 messages of three sizes from one Python process to a receiver process over localhost, once with UDP and once with TCP, and counted what arrived:
"""Send COUNT messages of SIZE bytes over localhost with UDP and with TCP.
The receiver runs in another process and counts what really arrives."""
import multiprocessing as mp, socket, sys, time
COUNT = 100_000
def udp_receiver(port, size, q, ready):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024)
s.bind(("127.0.0.1", port)); s.settimeout(1.0); ready.set()
got = 0
try:
while True:
s.recv(65536); got += 1
except socket.timeout:
pass
q.put(got)
def tcp_receiver(port, size, q, ready):
srv = socket.socket(); srv.bind(("127.0.0.1", port)); srv.listen(); ready.set()
conn, _ = srv.accept(); total = 0
while total < COUNT * size:
data = conn.recv(1 << 20)
if not data: break
total += len(data)
q.put((total // size, time.monotonic()))
def run(proto, size, port):
q, ready = mp.Queue(), mp.Event()
target = udp_receiver if proto == "udp" else tcp_receiver
p = mp.Process(target=target, args=(port, size, q, ready)); p.start(); ready.wait()
data = b"x" * size
if proto == "udp":
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
start = time.monotonic()
for _ in range(COUNT): s.sendto(data, ("127.0.0.1", port))
elapsed = time.monotonic() - start
got = q.get()
else:
s = socket.create_connection(("127.0.0.1", port))
start = time.monotonic()
for _ in range(COUNT): s.sendall(data)
got, end = q.get(); elapsed = end - start
p.join()
print(f"{proto} {size:>5} B: {COUNT/elapsed:>9,.0f} msg/s sent, {got/COUNT:6.1%} delivered")
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9100
for size in (100, 1400, 8000):
for proto in ("udp", "tcp"):
run(proto, size, port); port += 1
We ran it twice; the numbers of the two runs differ by less than 6%:
| Message size | UDP messages/s | UDP delivered | TCP messages/s | TCP delivered |
|---|---|---|---|---|
| 100 bytes | 53,788-55,403 | 100% | 323,061-341,262 | 100% |
| 1,400 bytes | 50,817-52,482 | 99.3-99.7% | 299,205-309,291 | 100% |
| 8,000 bytes | 43,072-44,953 | 97.2-99.2% | 91,334-94,371 | 100% |
TCP sent more messages per second, and UDP lost some. The reasons are the features this post is about. TCP is a byte stream: the kernel joins many small writes into large segments, so a hundred 100-byte messages can travel as one packet. UDP sends each message as its own datagram, with the full trip through the network stack every time. When the receiver falls behind, TCP's flow control slows the sender down, while the kernel drops UDP datagrams as soon as the receive buffer is full, and nobody is told.
Localhost has no network in between and almost no round-trip time. The test shows what system calls, buffering and scheduling cost with this Python code on this machine, not a general ranking of the protocols. On a real network, the round trips and the loss that the next sections describe decide the latency, and that is where UDP can be faster.
Where TCP's "Slowness" Comes From
TCP's performance characteristics come from features that UDP simply doesn't have:
Flow Control: TCP adjusts its sending rate based on the receiver's buffer space. This prevents overwhelming a slow receiver but adds computation and can throttle the sender.
Congestion Control: TCP backs off when it detects network congestion. This is good for network health but can significantly reduce throughput during congestion events.
Ordered Delivery: TCP maintains sequence numbers and reorders packets. If packet 5 arrives before packet 4, TCP holds packet 5 until packet 4 shows up. This head-of-line blocking can add latency.
Retransmission: When a packet is lost, TCP detects it (via timeout or duplicate ACKs) and resends it. The detection and retransmission add latency that compounds with RTT.
Here's what happens when a packet is lost:
TCP with 1% packet loss (100ms RTT):
Send packets 1-10
Packet 5 is lost
After ~100ms, sender realizes via missing ACK
Resend packet 5
Wait another ~100ms for ACK
Total delay: ~200ms for that packet
UDP with 1% packet loss:
Send packets 1-10
Packet 5 is lost
Application never knows (unless you implement detection)
No automatic recovery
When UDP Is Actually Faster
UDP shines in specific scenarios:
Small, Independent Messages: DNS queries, game state updates, or sensor data where each message is self-contained and occasional loss is acceptable.
# DNS query - perfect for UDP
def dns_lookup(domain):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2.0)
query = build_dns_query(domain)
sock.sendto(query, ('8.8.8.8', 53))
try:
response, _ = sock.recvfrom(512)
return parse_dns_response(response)
except socket.timeout:
return None # Retry or fail - simple logic
Multicast and Broadcast: TCP can't do multicast. If you need to send the same data to multiple receivers efficiently, UDP is your only option.
Real-Time Streams: Video conferencing or live streaming where old data is worthless. Better to skip a lost frame than wait for retransmission.
# Video streaming - late frames are worthless
def stream_video_udp(sock, video_source):
frame_num = 0
while True:
frame = video_source.get_next_frame()
# Add frame number so receiver can detect gaps
packet = struct.pack('!I', frame_num) + frame
sock.sendto(packet, receiver_address)
frame_num += 1
time.sleep(1/30) # 30 fps
# No waiting for ACKs - if a frame is lost,
# the next frame is more valuable than resending
When TCP Is Actually Faster
Counterintuitively, TCP can outperform UDP in several situations:
Reliable Bulk Transfer: If you need reliable delivery and implement your own acknowledgment system on UDP, you're basically rebuilding TCP poorly. TCP's implementation is highly optimized.
Congested Networks: TCP's congestion control prevents packet loss. If you send UDP too fast and cause packet loss, you waste bandwidth on data that never arrives.
Long-Lived Connections: The connection setup cost amortizes over time. A web API that maintains persistent connections doesn't pay the handshake cost repeatedly.
Building Reliability on UDP
If you need reliability with UDP, you'll add overhead that erodes the speed advantage:
class ReliableUDP:
"""A simplified reliable UDP implementation."""
def __init__(self, sock):
self.sock = sock
self.sequence = 0
self.pending_acks = {}
self.receive_buffer = {}
def send_reliable(self, data, addr):
"""Send data and wait for acknowledgment."""
seq = self.sequence
self.sequence += 1
# Add sequence number to packet
packet = struct.pack('!I', seq) + data
# Send and wait for ACK (with retries)
max_retries = 3
for attempt in range(max_retries):
self.sock.sendto(packet, addr)
# Wait for ACK
self.sock.settimeout(0.5)
try:
ack_packet, _ = self.sock.recvfrom(1024)
ack_seq = struct.unpack('!I', ack_packet[:4])[0]
if ack_seq == seq:
return True
except socket.timeout:
continue
return False # Failed after retries
# Now you've added:
# - Sequence numbers (like TCP)
# - Acknowledgments (like TCP)
# - Retransmission (like TCP)
# - Timeout logic (like TCP)
#
# You've rebuilt TCP's overhead, but less efficiently
This is why protocols like QUIC exist - they use UDP as a transport but implement TCP-like reliability features at the application layer with better performance characteristics than kernel-level TCP.
When Is It Appropriate to Use UDP Instead of TCP?
Use UDP when a late packet is worth less than a lost one, when every message stands on its own, or when one sender must reach many receivers at once. Use TCP, or QUIC, when every byte must arrive and in order: web pages, APIs, file transfers, databases, email and SSH.
| Traffic | Transport | Why |
|---|---|---|
| DNS queries | UDP first | One small question and one answer; the client asks again after a timeout. Large answers and zone transfers switch to TCP |
| Voice and video calls (WebRTC, SIP and RTP) | UDP | A resent audio packet arrives too late to play. The codec hides a small loss |
| Fast online games | UDP | The next position update replaces a lost one. Chat, purchases and results go over a reliable channel |
| Time sync (NTP) | UDP | Waiting for a retransmission would spoil the time measurement |
| Metrics such as StatsD | UDP | Fire and forget: the app never waits for the metrics server, and a few lost samples do not matter |
| VPN tunnels (WireGuard, OpenVPN by default) | UDP | The TCP connections inside the tunnel already resend lost data. TCP inside TCP resends twice and stalls |
| DHCP, mDNS and other discovery | UDP | Broadcast and multicast exist only for UDP, and a DHCP client has no IP address for a connection yet |
| HTTP/3 | QUIC over UDP | QUIC adds encryption and reliable streams on top of UDP, so one lost packet stalls only its own stream |
| Live video for viewers (YouTube, Twitch) | HTTP over TCP or QUIC | Players download segments of a few seconds and buffer them. Only low-latency calls need UDP |
Several of these are not plain UDP anymore. DNS over TLS and DNS over HTTPS run on TCP or QUIC, and browsers use HTTP/3 where the server offers it. When you choose UDP for your own protocol, you take over the jobs that TCP does for you:
- Loss and order. Add sequence numbers, so the receiver can see gaps and old packets, and timeouts with retries where a message must arrive.
- Rate. UDP has no congestion control. A sender that ignores loss can fill a link and push out other traffic, including its own.
- Size. Keep each datagram below the path MTU, about 1,200 bytes to be safe on the internet. A larger datagram is split into IP fragments or not sent at all, depending on the protocol and the socket settings, and a lost fragment loses the whole datagram.
- Spoofed senders. UDP has no handshake, so the source address of a packet can be false. A service that answers a small request with a large response can be used to flood someone else; DNS, NTP and memcached have all been used this way.
If your list of jobs grows toward all of TCP, use TCP or QUIC instead. The TCP vs UDP simulator shows lost and reordered packets side by side.
Practical Recommendations
Choose UDP when:
- You're sending small, independent messages
- Occasional data loss is acceptable
- You need multicast or broadcast
- You're implementing a custom protocol (like QUIC) with specific reliability needs
- Latency is more important than reliability
Choose TCP when:
- You need reliable, ordered delivery
- You're transferring files or bulk data
- You don't want to implement reliability logic yourself
- You need the connection state that TCP provides
- You're building a standard request-response API
The speed difference between UDP and TCP is about latency, not throughput. UDP gets the first byte out sooner and does not let one lost packet hold back the rest, but it does not move more data, and once you add the reliability your application needs, TCP's well-tested implementation often wins.
Your network conditions, message patterns, and reliability requirements matter far more than the raw protocol speed. Profile your specific use case before optimizing.
Related Resources
- Network Fundamentals: Layers, IP, TCP, UDP and Ports: TCP and UDP side by side on a practice network you build yourself
- TCP vs UDP simulator and packet journey simulator
- Close vs shutdown on a socket: how a TCP connection ends, and
SO_REUSEADDRafter a restart - How to simulate a slow network: add delay and loss to test both protocols
- Networking fundamentals quiz and TCP/IP interview questions
We earn commissions when you shop through the links below.
Bitrise
Managed runners for GitHub Actions
Bitrise Build Hub runs your GitHub Actions jobs on M4 Macs and AMD Linux machines. Change runs-on and keep your workflows, secrets and logs on GitHub.
Atomsized
AWS platform engineering and GitOps
Design and automation for reliable AWS and Kubernetes platforms, safer delivery workflows, and preview and UAT environments your engineers can understand and own.
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
SMTPfast
Developer-first email API
Send transactional and marketing email through a clean REST API. Detailed logs, webhooks, and embeddable signup forms in one dashboard.
QuizAPI
Developer-first quiz platform
Build, generate, and embed quizzes with a powerful REST API. AI-powered question generation and live multiplayer.
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorTags
Found an issue?
Related Posts
Also worth your time on this topic
Why SCTP Is Not Widely Used Despite Its Technical Advantages
Explore why Stream Control Transmission Protocol (SCTP) remains niche despite offering features that improve on both TCP and UDP, including NAT issues, lack of OS support, and ecosystem inertia.
TCP/IP Fundamentals
Explain the difference between TCP and UDP. When would you use each?
junior
Nginx Load Balancing and Reverse Proxy
Master load balancing and reverse proxy patterns with Nginx to build resilient, scalable infrastructure.
75 minutes