Interactive Reference

Go HTTP
Multiplexing

How Go's net/http server handles millions of concurrent connections. Goroutine-per-request, HTTP/2 stream multiplexing, connection pooling, and the real limits of throughput.

Goroutine-per-Request HTTP/1.1 vs HTTP/2 Connection Pooling Throughput Limits Backpressure & Timeouts ServeMux Routing
01 — Goroutine-per-Request

One Goroutine Per Connection

Go's net/http.Server spawns a new goroutine for every accepted TCP connection. Each goroutine costs ~2–8 KB of stack (grows as needed). This is how Go handles 100K+ concurrent connections on a single machine.

⚑ Key insight: goroutines are NOT OS threads. Go's runtime multiplexes ~1M goroutines onto a handful of OS threads (GOMAXPROCS). Context switching costs ~200ns vs ~1μs for OS threads.
Incoming Request
Goroutine (active)
Goroutine (waiting I/O)
OS Thread
Click to spawn goroutines
Goroutine initial stack
2 KB (Go 1.19+)
Grows dynamically up to 1 GB. Shrinks during GC. Compare: OS thread stack = 1–8 MB fixed
Max goroutines
~1M practical limit
No hard limit. 1M goroutines ≈ 2–8 GB RAM. Limited by memory and scheduler overhead
Scheduler model
M:N (GMP)
G = goroutine, M = OS thread, P = logical processor (GOMAXPROCS). Each P has a local run queue
Context switch cost
~200ns goroutine vs ~1–5μs OS thread
Goroutine switch only saves/restores ~12 registers + stack pointer. No kernel mode transition needed
Server code path
Accept → go serve(conn)
net/http.Server.Serve() calls ln.Accept() in a loop. Each conn gets: go c.serve(connCtx). That's it.
Interview one-liner
"Go uses goroutine-per-conn, not thread-per-conn"
This is why Go servers scale to 100K+ concurrent connections without epoll/kqueue user code. The runtime handles it via netpoller.
02 — HTTP/1.1 vs HTTP/2

Head-of-Line vs Multiplexing

HTTP/1.1 sends one request per TCP connection at a time (head-of-line blocking). HTTP/2 multiplexes many streams over a single TCP connection — parallel requests, no blocking.

⚑ Go's net/http transparently supports HTTP/2 when using TLS. The server auto-negotiates via ALPN. Client-side: http2.ConfigureTransport() or use default transport with TLS.
Blocked / Waiting
Active Transfer
TCP Connection
HTTP/2 Stream
Compare head-of-line blocking vs stream multiplexing
HTTP/1.1 concurrency
1 req/conn (pipelining broken)
Browsers open 6 TCP connections per host to workaround. Each conn has head-of-line blocking
HTTP/2 concurrency
100+ streams per conn
Default MaxConcurrentStreams=250 in Go. All streams share one TCP connection. Frame-level interleaving
Go auto-negotiation
ALPN "h2" over TLS
http.ListenAndServeTLS() auto-enables HTTP/2. No code changes needed. h2c (cleartext) needs explicit setup
HTTP/2 HOL still exists
At the TCP layer
If a TCP packet is lost, ALL streams on that connection stall waiting for retransmit. This is why HTTP/3 (QUIC) uses UDP with per-stream loss recovery
Server push
Deprecated in Chrome (2022)
http.Pusher interface exists in Go but rarely used. 103 Early Hints is the modern alternative for preloading
Frame types
DATA, HEADERS, SETTINGS, PING, GOAWAY
WINDOW_UPDATE for flow control. PRIORITY (removed in RFC 9113). HPACK header compression saves ~85% bandwidth on headers
03 — Connection Pooling

Transport Connection Reuse

Go's http.Transport maintains a pool of idle TCP connections per host. It reuses connections instead of creating new ones for every request — critical for high-throughput HTTP clients.

⚑ Critical mistake: creating a new http.Client per request. This disables connection pooling. Always reuse a single client. Also: you MUST read and close resp.Body or connections leak.
Idle Connection
Active Connection
New Connection (slow)
Queued Request
Watch connections being reused from the pool
MaxIdleConns
100 (default)
Total idle connections across all hosts. Set higher for high-throughput services calling many backends
MaxIdleConnsPerHost
2 (default!)
Dangerously low for microservices. Set to 100+ if you're making many requests to the same host
MaxConnsPerHost
0 (unlimited)
Total (active + idle) connections per host. Set this to prevent connection storms during traffic spikes
Connection leak
Forgetting resp.Body.Close()
If you don't read + close the body, the connection can't be returned to the pool. Always: defer resp.Body.Close() and io.Copy(io.Discard, resp.Body)
IdleConnTimeout
90s (default)
Idle connections are closed after 90s. Tune based on your keep-alive timeout. Should be less than server's keep-alive
Interview one-liner
"Reuse http.Client, close resp.Body"
The #1 Go HTTP performance bug: creating new clients. #2: leaking connections by not draining response bodies. Both tank throughput.
04 — Throughput Limits

How Many Requests Can Go Handle?

A Go HTTP server can handle 100K–1M+ requests per second on modern hardware. The bottleneck is almost never the HTTP layer — it's your handler code, database calls, or network I/O.

⚑ Benchmarks: net/http handles ~150K RPS for "hello world" on a single core. With 8 cores: 500K–1.2M RPS. Real apps: 10K–100K RPS depending on handler complexity.
Select a handler type to simulate throughput
Hello World
500K–1.2M RPS
Pure CPU-bound. Bottleneck: syscall overhead (epoll), memory allocation, GC. Use fasthttp for 2–3x more
JSON API
100K–300K RPS
json.Marshal allocates. Use jsoniter or sonic for 3–5x faster encoding. Pool buffers with sync.Pool
DB-backed API
5K–50K RPS
Bottleneck: DB connection pool (default 25 in sql.DB), query latency, network RTT. Tune MaxOpenConns
File descriptors
ulimit -n (default 1024!)
Each TCP connection = 1 fd. Set to 65535+ for high-concurrency servers. Also tune net.core.somaxconn, tcp_tw_reuse
GC pressure
GOGC=100 (default)
Lower GOGC = more frequent GC = lower latency spikes but higher CPU. GOMEMLIMIT (Go 1.19+) is the modern tuning knob
Scaling strategy
Vertical first, then horizontal
Go scales near-linearly with cores (GOMAXPROCS). A 16-core machine handles 8x a 2-core. After that: load balancer + replicas
05 — Backpressure & Timeouts

Without vs With Timeouts

Without timeouts, slow clients or backends cause goroutine leaks — each stuck goroutine holds memory and a connection forever. Go's net/http has ZERO default timeouts. You must set them.

⚑ CRITICAL: http.ListenAndServe has NO timeouts by default. A slowloris attack can exhaust all goroutines. Always set ReadTimeout, WriteTimeout, IdleTimeout, and use context deadlines.
Healthy Request
Stuck Goroutine
Timeout Kill
Memory Usage
Watch goroutines accumulate without timeouts
ReadTimeout
5–30s recommended
Time from accept to fully reading request (headers + body). Prevents slowloris. Set on http.Server{}
WriteTimeout
10–60s recommended
Time from reading request to finishing response write. Must be > your longest handler time. Includes ReadTimeout
IdleTimeout
60–120s recommended
Keep-alive timeout between requests on same connection. Defaults to ReadTimeout if not set. Close idle connections to free fds
Context deadline
ctx, cancel := context.WithTimeout()
Server timeouts kill the connection. Context deadlines cancel downstream work (DB queries, HTTP calls). Use both together.
http.TimeoutHandler
Middleware wrapper
http.TimeoutHandler(h, 30*time.Second, "timeout") — wraps a handler with a per-request deadline. Returns 503 on timeout
Goroutine leak detection
runtime.NumGoroutine()
Export as a /debug/vars or Prometheus metric. If NumGoroutine grows unbounded, you have a leak. Use goleak in tests
06 — ServeMux Routing

How Go Routes Requests

Go 1.22 introduced enhanced ServeMux with method matching and path parameters. Before that, the default mux was embarrassingly simple — just longest-prefix matching on the path.

⚑ Go 1.22+: mux.HandleFunc("GET /users/{id}", handler) — method + path params built-in. No need for chi/gorilla for basic routing anymore.
Click a request to see routing resolution
Pre-1.22 routing
Longest prefix match only
No method matching, no path params. /users/ matches /users/anything. This is why chi/mux/gorilla exist
Go 1.22+ patterns
"GET /users/{id}"
Method + path params. r.PathValue("id") extracts params. Wildcards: {path...} catches rest of URL
Precedence rules
Most specific wins
Exact method > any method. Exact path > wildcard. /users/new beats /users/{id}. Conflicts are compile-time errors
Middleware chain
Handler wrapping pattern
func middleware(next http.Handler) http.Handler — wrap handlers for logging, auth, CORS. Same pattern in chi, stdlib, echo
chi vs stdlib
chi for complex routing
chi adds: route groups, regex constraints, middleware per-group, OPTIONS handling. For simple APIs, stdlib is now sufficient (Go 1.22+)
Performance
ServeMux: O(n) patterns
Linear scan of registered patterns. Fine for <1000 routes. For 10K+ routes: use a radix tree router (chi, httprouter). But you almost never have that many