📮 Analogy
If TCP is a phone call, UDP is a stack of postcards. You write an address on each one and drop it in the mailbox — no dialing, no “are you there?”, no guarantee it arrives or arrives in order. But it’s instant and cheap, and each postcard is a complete message. DNS, online games, and video calls all ride on postcards.
Connectionless datagrams
UDP has no handshake and no connection state. You just send a self-contained datagram to an address, and the kernel ships it off. There’s no ACK, no retransmit, no ordering — if a packet is dropped on the way, neither side is told. In exchange you get the lowest possible latency and message boundaries for free: one WriteTo is exactly one datagram, received as exactly one ReadFrom.
graph LR C["client<br/>WriteTo(addr)"] -->|"datagram 1"| S["server<br/>ReadFrom"] C -->|"datagram 2 (may be lost)"| S C -->|"datagram 3 (may arrive first)"| S
The server side opens a packet socket with net.ListenPacket and loops on ReadFrom, which hands back both the bytes and the sender’s address so you know where to reply. One socket serves every client — there’s no Accept, no per-connection goroutine:
// server: one socket serves every client
pc, err := net.ListenPacket("udp", ":9000")
if err != nil { log.Fatal(err) }
defer pc.Close()
buf := make([]byte, 1500) // ~one Ethernet MTU; size to your datagrams
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil { continue }
msg := buf[:n]
pc.WriteTo([]byte("echo: "+string(msg)), addr) // reply to the sender
}
Sending datagrams from a client
A client can either net.Dial("udp", ...) (which pins a default peer so plain Write/Read work) or resolve an address and WriteTo it explicitly. Because there’s no connection, a UDP Read can block forever waiting for a reply that may never come — always set a deadline:
// option A: Dial pins a remote — Write/Read use it, but it's still connectionless
conn, err := net.Dial("udp", "127.0.0.1:9000")
if err != nil { log.Fatal(err) }
defer conn.Close()
conn.Write([]byte("ping"))
conn.SetReadDeadline(time.Now().Add(2 * time.Second)) // or you may wait forever
buf := make([]byte, 1500)
n, _ := conn.Read(buf)
fmt.Printf("got %d bytes: %s\n", n, buf[:n])
// option B: explicit address + DialUDP for ad-hoc destinations
raddr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:9000")
uc, _ := net.DialUDP("udp", nil, raddr)
defer uc.Close()
uc.Write([]byte("ping"))
Parsing a UDP address, in-process
Resolving a numeric address into a *net.UDPAddr is pure parsing — no DNS, no socket — so it’s safe to run anywhere. This is the same *net.UDPAddr you’d hand to DialUDP or get back from ReadFromUDP:
package main
import (
"fmt"
"net"
)
func main() {
// ResolveUDPAddr parses "host:port" into a *net.UDPAddr.
// A numeric IP literal needs NO DNS and NO network — pure parsing.
addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:9000")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("network:", addr.Network()) // "udp"
fmt.Println("string: ", addr.String()) // 127.0.0.1:9000
fmt.Println("ip: ", addr.IP) // 127.0.0.1
fmt.Println("port: ", addr.Port) // 9000
fmt.Println("loopback?", addr.IP.IsLoopback())
// An IPv6 literal is bracketed in the string form.
v6, _ := net.ResolveUDPAddr("udp", "[::1]:53")
fmt.Println("v6: ", v6.String()) // [::1]:53
fmt.Println("v6 port:", v6.Port) // 53
}
Designing a datagram payload
Since each datagram is one self-contained message, you decide its bytes. Text is fine, but binary protocols pack a fixed layout with encoding/binary — compact and unambiguous. Here we encode a tiny telemetry record and decode it back, exactly what you’d put in a datagram’s payload (pure in-memory, no socket):
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
// A fixed 8-byte datagram payload: [id uint32][temp int16][flags uint16].
type Reading struct {
ID uint32
Temp int16
Flags uint16
}
func main() {
out := Reading{ID: 42, Temp: -120, Flags: 0b101}
// encode → the bytes you'd hand to WriteTo
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, out)
fmt.Printf("datagram: % x (%d bytes)\n", buf.Bytes(), buf.Len())
// decode → what the receiver does after ReadFrom
var in Reading
binary.Read(bytes.NewReader(buf.Bytes()), binary.BigEndian, &in)
fmt.Printf("decoded: %+v\n", in)
}
Because UDP preserves message boundaries, one datagram = one of these records — no framing needed (that’s TCP’s problem). See framing & protocols for the binary-encoding details.
When to choose UDP
Reach for UDP when latency beats reliability and a lost packet is cheaper than waiting for a retransmit:
- DNS — one tiny request, one tiny reply; on loss you just ask again.
- Real-time games & VoIP — last week’s position is useless, so don’t pay to resend it.
- Metrics & telemetry (e.g. StatsD) — dropping the occasional sample is fine.
- QUIC / HTTP/3 — builds its own reliability, ordering, and encryption on top of UDP to dodge TCP’s head-of-line blocking.
- Multicast —
net.ListenMulticastUDPsends one datagram to a whole group; TCP can’t multicast at all.
Stick with TCP when every byte must arrive, in order, intact.
TCP vs UDP at a glance
| TCP | UDP | |
|---|---|---|
| Connection | yes (handshake) | none |
| Reliability | guaranteed, retransmits | none — may drop |
| Ordering | in-order | arbitrary |
| Boundaries | none (stream — you frame) | preserved (1 write = 1 datagram) |
| Server model | Accept + goroutine per conn | one socket, ReadFrom loop |
| Read API | Read | ReadFrom (returns sender addr) |
| Best for | files, APIs, anything exact | DNS, games, VoIP, metrics, QUIC |
⚠️ No delivery guarantees, and watch the datagram size
The playgrounds above are pure parsing/encoding — no socket, no network — so they’re safe anywhere. When you run the fenced examples locally, bind to a localhost/port you control. Two traps: (1) UDP gives no delivery, ordering, or dedup — if your app needs those, build them or use TCP/QUIC. (2) Keep datagrams small (often ≤ ~1400 bytes) to avoid IP fragmentation; an oversized datagram that fragments is dropped wholesale if any fragment is lost. And always SetReadDeadline — a reply may never come.
See also
- TCP sockets — the reliable, ordered, connection-based alternative.
- framing & custom protocols —
encoding/binaryand laying out a binary message. - DNS & addressing — DNS is the classic UDP application.
- fmt & io — the Reader/Writer model the
netpackage shares.
Next: turning names into addresses — DNS & addressing.
Related topics
The reliable byte stream — net.Dial and net.Listen, the Conn interface every connection implements (an io.Reader+Writer), framing a byte stream, read/write deadlines and net.Error, and one goroutine per connection.
net-basicsFraming & Custom ProtocolsTurning a TCP byte stream into messages — delimiter vs length-prefix framing, encoding/binary and network byte order, io.ReadFull, and designing a header-plus-payload protocol with a version field and checksum.
net-basicsDNS & AddressingFrom names to numbers — net.ParseIP and net/netip, SplitHostPort/JoinHostPort, CIDR prefixes and subnet membership, and the net.Resolver that turns hostnames into IPs over the network.
Check your understanding
Score: 0 / 51. What does UDP NOT provide that TCP does?
UDP is connectionless and fire-and-forget. There's no handshake, no acknowledgement, no retransmission, and no ordering. A datagram either arrives whole or not at all — your application handles loss, dedup, and ordering if it needs them.
2. Unlike TCP, what does UDP preserve about each WriteTo?
UDP is datagram-oriented: each WriteTo produces one datagram and the receiver gets it as one ReadFrom (or not at all). So unlike TCP you do NOT need to frame messages — but you still handle loss and reordering yourself.
3. Which workload is the best fit for UDP?
UDP shines when latency matters more than perfect delivery: DNS, real-time games, VoIP, telemetry/metrics, and QUIC (which builds HTTP/3 on UDP). For exact, ordered, must-arrive bytes, use TCP.
4. Why does a UDP server use ReadFrom rather than Read?
A single UDP socket serves every client — there's no per-connection object. ReadFrom hands back both the bytes and the *net.UDPAddr they came from, so WriteTo can send the reply to the right peer.
5. Why keep UDP datagrams small (often ≤ ~1400 bytes)?
If a datagram exceeds the path MTU it's split into IP fragments; if any fragment is lost, the entire datagram is discarded, so loss probability rises sharply. Staying under ~1400 bytes keeps each datagram to a single packet.
Comments
Sign in with GitHub to join the discussion.