Network Systems Overview
Network Systems Overview
This part treats networking in Go as a systems discipline: connections, budgets, failure domains, and operability — not merely how to call http.Get.
If earlier chapters taught you the standard library surface, this part teaches how network software behaves under load, packet loss, slow clients, and partial dependency failure.
Why This Part Exists
Most production outages involving Go services are not “Go is slow.” They are:
- Unbounded concurrency on accept/read paths
- Missing or inconsistent timeouts
- Retry storms that amplify a partial outage
- Proxies that hide backend health
- Protocol bugs (especially custom TCP framing)
You need a mental model that spans the stack:
client process
|
v
TCP/TLS session ---- deadlines, keepalive, congestion
|
v
application protocol ---- framing / HTTP semantics
|
v
service policy ---- auth, limits, routing, retries
|
v
upstream dependency ---- its own budgets and failures
Learning Path
| Chapter | Focus | You will be able to |
|---|---|---|
| 131 TCP services | Byte streams, framing, conn lifecycle | Build a correct custom TCP protocol handler |
| 132 HTTP resilience | Timeout budgets, client/server guardrails | Design end-to-end latency budgets without cascade |
| 133 Reverse proxy | Routing, upstream selection, blast radius | Place reliability policy at the edge |
Core Concepts Map
1. Streams vs messages
TCP delivers an ordered byte stream. Message boundaries are your problem. HTTP is a message protocol layered on TCP (and now HTTP/2/3 multiplex differently). Conflating stream and message is the root of many TCP bugs.
2. Deadlines are load shedding
Every Read/Write without a deadline is a potential goroutine leak under slowloris-style behavior. Deadlines are not only “correctness”; they are capacity protection.
3. Budgets flow downward
An inbound request with a 1.2s budget cannot safely give every dependency a fresh 1.2s. Propagate remaining time via context.Context and refuse work you cannot finish.
total budget: 1200ms
auth 50ms
cache 100ms
db 400ms
upstream 500ms (must use remaining, not full 1200)
encode 50ms
slack 100ms
4. Proxies are control planes
A reverse proxy is not “just nginx in front.” In Go (or any edge), it is where you centralize routing, timeouts, identity headers, and failure isolation so every backend need not reimplement policy.
Go-Centric Building Blocks
You will use these packages heavily:
| Package | Role |
|---|---|
net |
Listeners, dialers, TCP conn deadlines |
net/http |
Servers, clients, transports, reverse proxy helpers |
context |
Cancellation and deadline propagation |
io / bufio |
Framing, limited readers, efficient IO |
sync / golang.org/x/sync/errgroup |
Bounded fan-out, lifecycle |
crypto/tls |
Secure transport (deep dive in part 17) |
log/slog |
Structured operational logs |
Idiom: always bound the server
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}Idiom: always bound the client
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ForceAttemptHTTP2: true,
},
}Defaults without timeouts are the most common production footgun in Go HTTP code.
Production Mindset
Before writing a network feature, answer:
- What is the unit of work? (TCP frame, HTTP request, gRPC RPC)
- What is the timeout budget? (client SLA → service → dependencies)
- What fails? (timeout, cancel, protocol error, overload)
- What do we do on failure? (fail fast, retry once, degrade, shed load)
- What do we observe? (accepts, active conns, latency histograms, error classes)
If you cannot answer these, the implementation will invent answers under fire — usually the wrong ones.
Relationship to Other Parts
- Part 7 (concurrency) — worker pools, context, pipelines feed network handler design.
- Part 8 (web) — higher-level HTTP/API frameworks sit on the resilience patterns here.
- Part 15 (distributed infra) — retries, circuit breakers, and queues build on timeout budgets.
- Part 16 (observability) — correlation and metrics make network failures diagnosable.
- Part 17 (security) — TLS/mTLS protect the same sockets you harden for availability.
How to Study These Chapters
- Read 131 with a scratch TCP echo/framed server; break it deliberately (partial reads, huge frames).
- Read 132 while instrumenting a real
http.Serverandhttp.Clientunderheyorvegeta. - Read 133 by building a tiny reverse proxy with health-aware round-robin and watching failover.
Prefer one solid lab over ten passive reads. Network bugs only become intuitive when you have watched a stuck Read hold a goroutine for minutes.
Checklist: Network Systems Literacy
After this part you should confidently:
- Explain why TCP needs framing
- Set and reason about deadlines on both ends of a connection
- Draw a timeout budget for a multi-dependency request
- Configure
http.Serverandhttp.Transportfor production - Classify retryable vs non-retryable HTTP failures
- Describe how a reverse proxy reduces blast radius
- Name the metrics you would export for a TCP and HTTP service
Exercises (Part Warm-Up)
- List every network listener in a service you know; for each, write down timeouts or mark “missing.”
- Capture a pcap or use
ss/netstatduring a local load test; correlate ESTABLISHED count with goroutine count. - Sketch the failure modes of “client timeout 30s, server no timeout, dependency timeout 60s.”
- Write a one-page design note: where should retries live (client, proxy, both)? Defend one answer.
- Identify whether your public API needs custom TCP or whether HTTP/gRPC already provides your framing.
Lab Setup Recommendation
A single laptop is enough for this part:
# Terminal A: your experimental server
go run ./cmd/labserve
# Terminal B: load
hey -n 5000 -c 50 http://127.0.0.1:8080/api
# Terminal C: profiles (admin port)
go tool pprof -http=:18081 http://127.0.0.1:6060/debug/pprof/profile?seconds=15Capture goroutine counts (/debug/pprof/goroutine?debug=1) before and after introducing missing timeouts — the difference is the lesson.
Anti-Patterns to Unlearn
http.DefaultClientin libraries — no timeout, shared by the whole process.go handle(conn)without a bound — connection floods become memory floods.- Retries at every layer — browser × edge × app × client SDK.
- Health checks that only dial TCP — process up, app not ready.
- Logging full request bodies on the edge — cost and PII.
Glossary
- Deadline — absolute time when an IO op must fail.
- Timeout budget — remaining time for a request tree.
- Framing — how messages are delimited on a byte stream.
- Keep-alive — reusing connections; needs idle limits.
- Load shedding — refusing work to protect capacity.
- Blast radius — how far a failure propagates.
More examples
Tour: dial timeout + httptest client budget
One mini-program that touches the part’s themes: bounded dial, HTTP round-trip with context, and fail-fast on deadline.
mkdir -p /tmp/go-net-tour && cd /tmp/go-net-tour
go mod init example.com/net-tourSave as main.go:
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"time"
)
func main() {
// 1) Dial with timeout (refused port → quick error, not hang)
d := net.Dialer{Timeout: 100 * time.Millisecond}
_, err := d.Dial("tcp", "127.0.0.1:1")
fmt.Println("dial fail fast:", err != nil)
// 2) Server + client with request context budget
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "pong")
}))
defer ts.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Print("http: ", string(b))
}go run .Expected output:
dial fail fast: true
http: pong
Runnable example
Tour of this part: a tiny HTTP server and client with production-shaped timeouts—the shared foundation for TCP framing, HTTP resilience, and reverse proxies.
mkdir -p /tmp/go-net-tour && cd /tmp/go-net-tour
go mod init example.com/net-tourSave as main.go:
package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "pong")
})
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
srv := &http.Server{
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
go func() { _ = srv.Serve(ln) }()
client := &http.Client{
Timeout: 3 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: time.Second}).DialContext,
TLSHandshakeTimeout: 2 * time.Second,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
},
}
url := "http://" + ln.Addr().String() + "/api"
resp, err := client.Get(url)
if err != nil {
log.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("status=%d body=%q\n", resp.StatusCode, string(body))
fmt.Println("tour: timeouts set on server and client")
_ = srv.Close()
}go run .Expected output:
status=200 body="pong\n"
tour: timeouts set on server and client
What to notice
- Default clients and servers without timeouts are the #1 production footgun.
- This part stacks TCP framing (131), budgeted HTTP (132), and edge policy (133) on these primitives.
Try next
- Remove
client.Timeoutand point at a black-hole address; restore the timeout. - Read chapter 131 and implement length-prefix framing on a raw TCP conn.
Next Chapter
Start with TCP Services Deep Dive — the lowest layer of the model, and the place most custom-protocol bugs hide.