Networking & HTTP

UDP & DatagramSocket

15 min Lesson 3 of 13

UDP & DatagramSocket

User Datagram Protocol (UDP) is the second major transport-layer protocol alongside TCP. Where TCP gives you a reliable, ordered, connection-oriented byte stream, UDP gives you fast, connectionless, best-effort datagrams. Understanding both — and knowing when each is the right tool — is essential for any experienced Java developer building networked systems.

What UDP Actually Is

UDP sends individual datagrams: self-contained packets that carry a destination address, a source port, a destination port, a checksum, and a payload. Each datagram is routed independently. The protocol makes no guarantee that a packet arrives, arrives only once, or arrives in order. There is no handshake, no connection state, and no retransmission.

UDP overhead is tiny. A UDP header is only 8 bytes. A TCP header is 20–60 bytes, and TCP's connection establishment (the three-way handshake) adds at least one full round-trip before any data moves. For high-frequency, latency-sensitive traffic that can tolerate occasional loss, UDP wins on raw performance.

When to Choose UDP Over TCP

UDP is the right choice when speed and low latency matter more than reliability, or when the application can handle lost or reordered packets better than the operating system can. Classic use cases include:

  • Real-time audio and video (VoIP, video conferencing, live streaming) — a slightly degraded frame is better than a stalled stream waiting for retransmission.
  • Online multiplayer games — the position update from 50 ms ago is useless; send a fresh one instead of retransmitting the old one.
  • DNS — queries and responses fit in a single packet; the client simply retries if no reply arrives.
  • DHCP, SNMP, TFTP, NTP — protocols that are simple enough to manage their own reliability, or where low overhead outweighs occasional loss.
  • Metrics and telemetry — losing a counter snapshot occasionally is acceptable; saturating the network with TCP overhead is not.
  • Broadcast and multicast — UDP supports sending a single datagram to many recipients; TCP is point-to-point only.
QUIC (HTTP/3) is UDP under the hood. Google designed QUIC — the transport for HTTP/3 — on top of UDP and implements its own reliability and multiplexing. This lets it avoid TCP's head-of-line blocking while still delivering ordered streams where needed. Modern internet traffic is increasingly UDP-based for exactly this reason.

Java's DatagramSocket and DatagramPacket

Java exposes UDP through two classes in java.net:

  • DatagramSocket — the socket that sends and receives datagrams. Bind it to a local port to receive; leave it unbound (or let the OS assign a port) to send-only.
  • DatagramPacket — a container for one datagram: byte array, offset, length, and optionally an InetAddress and port (for outbound packets).

Building a UDP Echo Server

The server binds to a port, loops receiving packets, and echoes each one back to the sender's address and port — which UDP provides automatically in the received packet.

import java.net.DatagramPacket; import java.net.DatagramSocket; public class UdpEchoServer { private static final int PORT = 9000; private static final int BUFFER_SIZE = 1024; public static void main(String[] args) throws Exception { try (DatagramSocket socket = new DatagramSocket(PORT)) { System.out.println("UDP echo server listening on port " + PORT); byte[] buffer = new byte[BUFFER_SIZE]; while (true) { DatagramPacket request = new DatagramPacket(buffer, buffer.length); socket.receive(request); // blocks until a datagram arrives String received = new String( request.getData(), 0, request.getLength() ); System.out.println("Received: " + received + " from " + request.getAddress() + ":" + request.getPort()); // echo back: reuse address and port from the received packet DatagramPacket response = new DatagramPacket( request.getData(), request.getLength(), request.getAddress(), request.getPort() ); socket.send(response); } } } }
Always pre-allocate a fresh receive buffer — or reset the packet length. After socket.receive(packet) the packet's length is set to the number of bytes actually received, which may be smaller than the buffer. If you re-use the same DatagramPacket for the next receive call without resetting its length to the full buffer size, receive() will only read into that smaller slice and silently truncate larger packets. Call packet.setLength(buffer.length) at the start of each loop iteration.

Building a UDP Client

The client creates an unbound socket (the OS assigns an ephemeral source port), sends a datagram to the server's address and port, then receives the echo.

import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; public class UdpEchoClient { private static final String SERVER_HOST = "localhost"; private static final int SERVER_PORT = 9000; private static final int TIMEOUT_MS = 3_000; public static void main(String[] args) throws Exception { String message = "Hello, UDP!"; byte[] sendBuffer = message.getBytes(); try (DatagramSocket socket = new DatagramSocket()) { socket.setSoTimeout(TIMEOUT_MS); // avoid blocking forever InetAddress serverAddress = InetAddress.getByName(SERVER_HOST); DatagramPacket request = new DatagramPacket( sendBuffer, sendBuffer.length, serverAddress, SERVER_PORT ); socket.send(request); byte[] receiveBuffer = new byte[1024]; DatagramPacket response = new DatagramPacket(receiveBuffer, receiveBuffer.length); socket.receive(response); // blocks until reply or timeout String reply = new String( response.getData(), 0, response.getLength() ); System.out.println("Echo: " + reply); } } }

Handling Packet Loss in Application Code

Because UDP provides no retransmission, any reliability the application needs must be implemented above the protocol. The standard pattern is send with timeout and retry:

import java.io.IOException; import java.net.*; public class UdpReliableSender { private static final int MAX_RETRIES = 3; private static final int TIMEOUT_MS = 2_000; public static byte[] sendWithRetry( DatagramSocket socket, DatagramPacket request, int responseBufferSize) throws IOException { byte[] responseBuffer = new byte[responseBufferSize]; DatagramPacket response = new DatagramPacket(responseBuffer, responseBuffer.length); for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { socket.setSoTimeout(TIMEOUT_MS); socket.send(request); socket.receive(response); return response.getData(); // success } catch (SocketTimeoutException e) { System.err.printf("Attempt %d timed out, retrying...%n", attempt); } } throw new IOException("No response after " + MAX_RETRIES + " attempts"); } }

Broadcast and Multicast

UDP supports two forms of one-to-many delivery that TCP cannot provide. Broadcast sends a datagram to every host on a subnet. Multicast sends to a group identified by a class-D IP address (224.0.0.0 – 239.255.255.255); only hosts that have joined the group receive the packets.

import java.net.*; // Broadcast sender (enable with setBroadcast) public class BroadcastSender { public static void main(String[] args) throws Exception { try (DatagramSocket socket = new DatagramSocket()) { socket.setBroadcast(true); String msg = "Service available on port 8080"; byte[] data = msg.getBytes(); // 255.255.255.255 = limited broadcast InetAddress broadcastAddress = InetAddress.getByName("255.255.255.255"); DatagramPacket packet = new DatagramPacket( data, data.length, broadcastAddress, 4567 ); socket.send(packet); System.out.println("Broadcast sent."); } } }

For multicast, use MulticastSocket (a subclass of DatagramSocket) and call joinGroup() on receivers:

import java.net.*; public class MulticastReceiver { public static void main(String[] args) throws Exception { InetAddress group = InetAddress.getByName("230.0.0.1"); try (MulticastSocket socket = new MulticastSocket(5000)) { socket.joinGroup(group); byte[] buffer = new byte[256]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); System.out.println("Multicast message: " + new String(packet.getData(), 0, packet.getLength())); socket.leaveGroup(group); } } }

Key Trade-offs to Remember

  • No ordering guarantee — packets can arrive out of sequence. If order matters, add sequence numbers in your payload and sort at the receiver.
  • No delivery guarantee — implement retransmission or accept loss, depending on the use case.
  • No flow control or congestion control — a fast sender can overwhelm a slow receiver or a narrow link. You are responsible for rate-limiting.
  • Maximum datagram size — the UDP payload limit is 65,507 bytes (65,535 minus IP and UDP headers). For reliable large-data transfer, fragment at the application layer or use TCP.
  • Thread safetyDatagramSocket is not thread-safe. Use one socket per thread, or synchronize explicitly.
Consider NIO for high-throughput UDP servers. java.nio.channels.DatagramChannel lets you use non-blocking I/O and a selector, multiplexing thousands of peers on a single thread — the same approach that powers high-performance game servers and streaming infrastructure.

Summary

UDP trades reliability for speed. Java's DatagramSocket and DatagramPacket give direct, low-overhead access to this protocol. Use UDP when latency matters more than guaranteed delivery, when the payload fits in one packet, or when broadcast and multicast are required. Implement any necessary reliability — retransmission, ordering, deduplication — in your application layer, and set a SO_TIMEOUT on every receive call to prevent indefinite blocking.