AlgoMaster Logo

UDP Server

Low Priority10 min readUpdated June 6, 2026

UDP is the other transport protocol Go's net package speaks fluently. Unlike TCP, it's connectionless, message-oriented, and offers no delivery guarantees, which makes it a fit for telemetry, price ticks, and quick availability probes where speed beats reliability. This lesson covers how to build a UDP server in Go, how its single-socket model differs from TCP, and what trade-offs come with packet loss and size limits.

When UDP Beats TCP

TCP is a connection-oriented stream protocol. It sets up a session, guarantees ordering, retries lost segments, and tears the connection down at the end. That bookkeeping costs round-trips and memory per connection.

UDP skips all of it. A UDP message, called a datagram, is sent as a single, self-contained packet. The sender writes a chunk of bytes, the receiver gets that exact chunk or nothing at all. There's no handshake, no acknowledgment, no retry. If a router drops the packet, it's gone.

This sounds bad until you look at what UDP is actually used for:

Use CaseWhy UDP Fits
DNS lookupsSingle request, single response, retry handled at the app level
Price tick broadcastsLatest value matters more than every value
Inventory availability pingsCheap to ask, cheap to retry
Game state updatesStale data is better than late data
Metrics and logs (StatsD)Losing a metric sample is acceptable

The two protocols compare as follows:

PropertyTCPUDP
ConnectionRequired (handshake)None
DeliveryGuaranteedBest-effort
OrderingGuaranteedNone
BoundariesStream, no message boundariesPreserves message boundaries
Overhead per packet~20 bytes header~8 bytes header
Goroutine model in GoOne per connectionOne per socket
Use whenYou can't lose dataYou can't afford the wait

Listening for Datagrams

In Go, you open a UDP socket with net.ListenPacket. It returns a net.PacketConn, which is the connectionless cousin of net.Listener.

There's no Accept call, because there are no connections to accept. The socket is now ready to receive datagrams from any peer that sends one.

To read a datagram, call ReadFrom:

ReadFrom blocks until a datagram arrives. It returns three things:

  • n: how many bytes the datagram carried, written into buf.
  • addr: the sender's address, which you'll need to reply.
  • err: any error from the socket itself.

The bytes you care about are buf[:n]. Anything past index n is leftover from a previous read or zero-initialized.

To reply, call WriteTo with the sender's address:

There is only one socket and one address per call. The server doesn't open a return channel to the client. It just sends a datagram back to wherever the request came from.

UDP has no flow control. If your ReadFrom loop falls behind, the kernel's receive buffer fills up and packets are silently dropped. There's no error, no retry, nothing. Keep the read loop tight, and offload work to other goroutines.

One Socket, One Goroutine

With TCP, the pattern was: Accept returns a new net.Conn per client, you launch a goroutine per connection, and each goroutine reads from its own conn until the client closes. That model works because TCP gives you one stream per peer.

UDP is different. There's no per-peer state. All datagrams arrive at the same socket regardless of who sent them. You can't spawn a goroutine per peer at the listening stage, because the listener never sees peers separately.

So the typical UDP server looks like this:

One goroutine reads from the socket. If individual requests take any real work, you hand the data off to another goroutine and go back to reading. The read loop's only job is to drain the socket fast enough that the kernel doesn't drop packets.

The diagram shows the shape clearly. Three clients fire datagrams at port 9000. All of them land in the same socket. A single goroutine pulls them out one at a time, then dispatches each one to a worker goroutine for processing.

Buffer Sizes and Fragmentation

Picking the read buffer size matters more in UDP than in TCP. With TCP, you can keep reading until you've got everything. With UDP, one ReadFrom call gets exactly one datagram. If the datagram is bigger than your buffer, the extra bytes are truncated and lost.

The practical upper bound on a single UDP payload is the path MTU minus the IP and UDP headers. On a typical Ethernet network:

  • Ethernet MTU: 1500 bytes
  • IPv4 header: 20 bytes
  • UDP header: 8 bytes
  • Safe payload: 1472 bytes

Going larger forces IP-level fragmentation, where the OS splits the datagram into multiple IP packets. If even one fragment is lost, the whole datagram is dropped, since UDP doesn't retry. Most production UDP services keep payloads under 1472 bytes for this reason.

A 1500-byte buffer is a common default. It's the full MTU, so you won't truncate a maximum-sized payload, and the extra few bytes are negligible.

If your protocol uses smaller messages (a typical metrics line is well under 100 bytes), a smaller buffer is fine and saves allocations in a hot read loop.

A Price-Tick Server, End to End

Time to put the pieces together. The server below is a price-tick broadcaster for a tiny inventory of products. A client sends PING <productID>, and the server replies with the current price as PRICE <productID> <cents>. The whole thing runs inside main(), with the server in a goroutine and a few client requests right after.

The server uses net.ListenPacket, which is unconnected: it can receive from any peer and reply to any peer. The client uses net.Dial("udp", ...), which returns a net.Conn bound to a single remote address. The bound socket is convenient for clients because Read and Write don't need to specify an address every call, and reads automatically filter out datagrams from anyone else.

The client also sets a read deadline. UDP has no built-in timeout, so without a deadline a lost reply would leave conn.Read blocked forever. Five hundred milliseconds is generous for localhost but realistic for handling packet loss on a real network.

The server's error replies (ERR bad request, ERR unknown kiwi) are an application-level convention, not a transport feature. UDP itself doesn't know anything about errors. If you want the client to learn that a product is unknown, you have to send a payload that says so.

Living With Packet Loss

UDP delivers a datagram or it doesn't. There's no "delivered late" state. A well-behaved UDP service plans for loss instead of pretending it can't happen.

Three patterns are common:

  • Idempotent requests. If a request can safely be sent twice, the client retries on timeout. The price-tick example above is naturally idempotent: asking for the price of apple twice produces the same answer.
  • Sequence numbers. When ordering matters, the sender stamps each datagram with a counter. The receiver can detect gaps and decide whether to ignore, request a resend, or accept stale data.
  • Periodic broadcasts. When the latest value is what counts, the server simply re-sends the current state on a schedule. A subscriber that missed one tick gets the next one.

What you typically should not do is build a generic reliability layer on top of UDP. If you need ordered, guaranteed delivery, you're rebuilding TCP, and TCP exists.

For shutting the server down cleanly, set a read deadline before each loop iteration and check a stop signal:

The deadline causes ReadFrom to return with a timeout error every second, giving the loop a chance to notice the done channel was closed. There's no equivalent of http.Server.Shutdown for raw UDP. For UDP, plain pc.Close() is the shutdown mechanism, and deadlines are how you make the loop responsive in the meantime.

Quiz

UDP Server Quiz

10 quizzes