C.W.K.
Stream
Lesson 04 of 05 · published

Compression & Keep-Alive — Free Bytes and Free Round Trips

~10 min · caching-perf, compression, gzip, brotli, keep-alive

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Two of HTTP's oldest performance wins are still its biggest: compress the body, reuse the connection. Both are negotiated on the wire; both cost almost nothing to enable."

Content Compression — Negotiated, Transparent

HTTP body compression is negotiated via two headers:

  • Accept-Encoding (request) — what compressions the client can decode: gzip, br, zstd, identity.
  • Content-Encoding (response) — what the server actually used.

Browsers always send Accept-Encoding. Servers pick the best supported encoding and compress the body before sending. Clients decompress transparently — the application code sees decoded text.

Three algorithms in active use:

  • gzip — universally supported (every HTTP client and server). 50-70% reduction on text content. Fast encode/decode. The safe default.
  • Brotli (br) — better compression (15-25% more than gzip on text), comparable decode speed, slower high-quality encode. Almost universally supported in 2026 (every modern browser + most CDNs).
  • Zstandard (zstd) — newer, very fast at moderate quality, growing in HTTP support but not yet universal.

Pre-Compress Static, Compress-Per-Request Dynamic

The cost asymmetry between encoding and decoding matters:

  • Static assets — pre-compress at build time with highest-quality Brotli. Ship the .br file directly when Accept-Encoding allows. One-time CPU cost amortized across every request.
  • Dynamic responses — compress per-request with moderate quality gzip or Brotli (level 4-6). The CPU/bandwidth tradeoff usually favors compression for text/JSON over ~1KB.

Don't compress already-compressed content (images, video, .tar.gz) — gain is zero, CPU is wasted, and re-compression can sometimes increase size.

Keep-Alive — The Connection You Don't Throw Away

HTTP/1.0's default was "open a TCP connection, send one request, get one response, close." The TCP handshake (1 RTT) + TLS handshake (1-2 RTTs) ate every request. For a page with 60 assets, that's 180+ RTTs of pure connection setup.

HTTP/1.1's Connection: keep-alive (the default) keeps the TCP connection open after the response, letting subsequent requests reuse it. One handshake amortized over N requests. The numbers: a remote server at 100ms RTT goes from ~300ms per request (HTTP/1.0) to ~100ms per request (HTTP/1.1 keep-alive) without changing anything else.

HTTP/2 takes this further with multiplexing (next lesson) — one connection carries many parallel requests. HTTP/3 with QUIC adds connection migration (your phone can switch from Wi-Fi to LTE without losing the session).

Compression and connection reuse are 80% of HTTP performance. Before you reach for fancier optimizations (server push, HTTP/3, edge functions), make sure both are on. They're free and they're huge.

The Connection-Header Gotchas

1. Connection: close on every response. Some legacy servers (or proxies) emit Connection: close after every request, forcing a new TCP setup. Look for it; fix it.

2. Long-lived connections leaking server resources. Server-side, keep-alive has a TTL (usually 60-300s). After the TTL, the server closes idle connections. Tune based on your client patterns.

3. SSL session resumption. Even with keep-alive, new connections incur TLS handshake. Modern TLS (1.3) supports session resumption — a returning client skips the key exchange. Make sure your server enables it.

cwkPippa's Compression Reality

cwkPippa's FastAPI/Uvicorn supports gzip compression via Starlette's GZipMiddleware — enabled with a 1KB minimum threshold so tiny responses aren't compressed (the CPU/byte tradeoff doesn't pay below ~1KB). Static assets served by Vite get pre-compressed Brotli files in build output; the dev server serves them with proper Content-Encoding. Keep-alive is on by default in Uvicorn. cwk-site on Vercel does the same automatically — Brotli for text, gzip fallback, keep-alive everywhere. The wins are real but invisible to the application code; they're transport-layer concerns the framework handles.

Code

curl --compressed lets you measure the compression win·bash
# Negotiate compression with curl
curl -v --compressed https://creativeworksofknowledge.com/ \
  2>&1 | grep -E '(Accept-Encoding|Content-Encoding)'
# > Accept-Encoding: deflate, gzip, br, zstd
# < content-encoding: br
# (curl auto-decompresses; the body you see is plaintext)

# Without --compressed, curl doesn't ask for compression
curl -v https://creativeworksofknowledge.com/ 2>&1 | grep -i encoding
# > (no Accept-Encoding sent → server returns plain text)

# Measure the win: byte size with vs without compression
echo 'with brotli:'  ; curl -s --compressed https://creativeworksofknowledge.com/ | wc -c
echo 'without:'     ; curl -s              https://creativeworksofknowledge.com/ | wc -c
FastAPI GZipMiddleware — automatic compression with a min-size guard·python
# FastAPI — enable gzip compression for responses
from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware

app = FastAPI()

# minimum_size avoids the CPU cost of compressing tiny responses
app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=6)

@app.get('/api/posts')
async def list_posts():
    return {'items': [...]}  # response body > 1KB → gzipped automatically

# For brotli, use brotli-asgi or serve pre-compressed files via a reverse proxy
Client connection reuse — Session/Client pattern saves handshakes·python
# Client side — reuse connections with httpx.Client (the recommended pattern)
import httpx
import time

# WRONG: new connection per call
start = time.perf_counter()
for i in range(5):
    httpx.get('https://api.example.com/users/' + str(i))  # new TCP+TLS every time
print(f'no reuse: {(time.perf_counter() - start) * 1000:.0f}ms')

# RIGHT: reuse one connection via Client
start = time.perf_counter()
with httpx.Client() as c:
    for i in range(5):
        c.get('https://api.example.com/users/' + str(i))  # one TCP+TLS handshake total
print(f'reuse:    {(time.perf_counter() - start) * 1000:.0f}ms')

# For HTTP/2 multiplexing benefit, add http2=True
with httpx.Client(http2=True) as c:
    c.get('https://api.example.com/users/42')

External links

Exercise

Take any JSON endpoint serving > 5KB of data (your own or any public API). Hit it three ways: (1) curl with no encoding negotiation (plain), (2) curl with --compressed (gzip/br negotiation), (3) curl -w '%{size_download}/%{size_header}\n' to measure body+header sizes. Calculate the compression ratio. Then write a Python client using httpx.Client() to reuse connections across 100 requests; compare wall-clock time to a 100-call loop creating a new client each time.
Hint
On a typical JSON list endpoint, expect 60-80% body size reduction from compression. Connection reuse on a remote server (>50ms RTT) typically saves 80-100ms per request after the first — for 100 requests, the difference is dramatic (10+ seconds of total handshake time eliminated). Both wins are completely transparent to your application code; you just enable them and they happen.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.