{} The Go Reference

Net basics · Web · Beginner

UDP Sockets

Connectionless datagrams in Go — net.ListenPacket and ReadFrom/WriteTo, DialUDP, message boundaries with no delivery guarantees, encoding a datagram payload, MTU and fragmentation, and when UDP beats TCP.

Net basics Beginner ⏱ 6 min read Complete

📮 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:

udpaddr.go — editable & runnable
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):

datagram.go — editable & runnable
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.
  • Multicastnet.ListenMulticastUDP sends 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

TCPUDP
Connectionyes (handshake)none
Reliabilityguaranteed, retransmitsnone — may drop
Orderingin-orderarbitrary
Boundariesnone (stream — you frame)preserved (1 write = 1 datagram)
Server modelAccept + goroutine per connone socket, ReadFrom loop
Read APIReadReadFrom (returns sender addr)
Best forfiles, APIs, anything exactDNS, 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

Next: turning names into addresses — DNS & addressing.

Check your understanding

Score: 0 / 5

1. 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.