Skip to content

Modern Protocols & Service Mesh

← Networking Overview


Why This Exists

HTTP/1.1 shipped in 1997. HTTP/2 in 2015. HTTP/3 is shipping now (2024). gRPC is the modern inter-service protocol. Service meshes handle networking that was previously "just TCP" and invisible. Zero-trust means every network hop requires authentication and encryption.

This page is what changed in networking from 2020 to 2025, and what you need to know about operating systems at that scale.


Part 1: HTTP/3 and QUIC — The Modern Transport

The Problem HTTP/2 Doesn't Solve

HTTP/2 multiplexing over TCP:

Stream 1: [Request] ──→ [Response]
Stream 2: [Request] ──→ [Response]
Stream 3: [Request] ──→ [Response]

One TCP connection carries all streams.
All streams share one congestion window.

If one packet is lost:
  TCP waits for retransmission (1-10ms typical)
  ALL streams wait
  → Slow page (63% slow pages, tail amplification)

HTTP/3 replaces TCP with QUIC (a custom transport layer on UDP).

QUIC: The Key Insight

TCP: Ordered byte stream
     Lost packet → entire stream stalled (HOL blocking)

QUIC: Multiplexed streams over UDP
      Lost packet in stream 1 → stream 1 stalls
      Streams 2, 3, 4 keep moving
      → No head-of-line blocking

Architecture:

HTTP/3 layer     (requests/responses, semantics)
QUIC layer       (streams, congestion control, connection migration)
UDP layer        (datagram transport)
IP layer

Performance Gains

Metric HTTP/2 over TCP HTTP/3 over QUIC
0-RTT resumption No (new connection = new handshake) Yes (client caches key from prev session)
Head-of-line blocking Yes (lost TCP packet stalls all streams) No (each stream independent)
Connection migration No (IP/port change = new connection) Yes (client can switch networks; connection persists)
Handshake latency TCP: 1 RTT + TLS: 1 RTT = 2 RTT QUIC first connection: 1-RTT. 0-RTT is resumption (replay-sensitive)
Packet loss tolerance One lost packet = cascading retransmits One lost packet affects only that stream

When HTTP/3 Matters

Mobile networks (cellular):

User on train, switches from LTE to WiFi.
HTTP/2: Connection dies → reconnect → new TLS handshake (1+ RTT)
HTTP/3: Connection persists → seamless handoff

High packet loss networks:

Scenario: WiFi with 1% packet loss
100 requests per page × 1% loss = 63% probability of at least one lost packet

HTTP/2: One loss → all streams pause → user waits
HTTP/3: One loss → only affected stream pauses → page loads

Low latency is critical (< 100ms SLO):

HTTP/2: 3-4 RTT (DNS, TCP, TLS, request/response)
QUIC first connection: 1-RTT handshake (not 0-RTT)
QUIC 0-RTT: resumption only — replay-sensitive; servers must treat 0-RTT data carefully
Savings vs TCP+TLS 1.2: on the order of 1 RTT → user-perceptible on high-latency links

Operational Gotchas

1. NAT/firewall breaks UDP: Many corporate firewalls drop UDP. QUIC works, but some clients fall back to HTTP/2.

2. QUIC CPU cost: QUIC offloading to NIC is still new. Brokers may see higher CPU usage.

3. Client support: Safari/Chrome full support (2024). Mobile clients lag. Fall-back to HTTP/2 is transparent.

Implementation:

// Go HTTP/3 server (quic-go)
import "github.com/quic-go/quic-go/http3"

// Listen on QUIC
quicServer := &http3.Server{
    Addr:    ":443",
    Handler: mux,
}
go quicServer.ListenAndServe()

// TLS/1.3 required
// cert/key management same as HTTP/2


Part 2: gRPC — Inter-Service Communication

Why gRPC > REST for Internal Services

REST to fetch user details:
  GET /users/123
  Response: {"id": 123, "email": "alice@example.com", "name": "Alice", ...}

Serialization: JSON encoding/decoding on every call
Overhead: Text format, schema discovery (OpenAPI), per-endpoint versioning

gRPC to fetch user details:
  service UserService {
    rpc GetUser(UserID) returns (User);
  }

  // Wire format: Protocol Buffers (binary)
  // Same payload: ~200 bytes JSON vs ~50 bytes protobuf (4× smaller)
  // Schema codegen at compile time (no runtime reflection)
  // Single connection carries multiple concurrent calls (HTTP/2 multiplexing)

gRPC Performance

Metric REST + JSON gRPC + Protobuf
Payload size 1000 bytes 250 bytes
Serialization time ~100 µs (encoding) + 100 µs (decoding) ~10 µs (encoding) + 10 µs (decoding)
Connections per server 1 per client (connection pooling) 1 per client (shared HTTP/2)
Max concurrent calls Pool size limited Unlimited (HTTP/2 streams)
Latency 5-10 ms typical 1-2 ms typical

Real-world example (100 microservices calling each other):

REST setup: Each call
  DNS (cached): 0
  TCP (pooled): 0
  TLS (pooled): 0
  HTTP overhead: ~50 bytes headers
  JSON serialization: 100 µs
  Network: 1 ms (local)
  Total: ~1.15 ms per call

gRPC setup: Each call
  DNS/TCP/TLS: 0 (reused)
  HTTP/2 framing: ~10 bytes overhead
  Protobuf serialization: 10 µs
  Network: 1 ms
  Total: ~1.01 ms per call

Per call, serialization is microseconds, not milliseconds. Do not multiply
µs/call by 1M calls/sec and call it "140 ms overhead" — that product is
*CPU-seconds per wall-second* (how many cores you burn), not added latency.
At 1M calls/sec, 140 µs extra *serialize* work is ~140 cores of CPU, while
each call still sees ~1 ms of network. Quote latency and CPU separately.

When to Use gRPC

✓ Internal service-to-service (your code controls both ends)
✓ High throughput (>10k RPS per connection)
✓ Latency-sensitive (<10ms p99)
✓ Bidirectional streaming (gRPC native support)
✓ Language diversity (protobuf code-gen for Go, Java, Python, Rust, etc.)

✗ Public APIs (clients expect REST, JSON)
✗ Browser clients (browsers don't fully support gRPC-web)
✗ Simple request/response (REST is adequate)

gRPC Server and Client

// Server
type UserServiceImpl struct{}

func (s *UserServiceImpl) GetUser(ctx context.Context, 
    req *pb.UserID) (*pb.User, error) {
    // Fetch user
    return &pb.User{
        Id:    req.Id,
        Name:  "Alice",
        Email: "alice@example.com",
    }, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":50051")
    server := grpc.NewServer()
    pb.RegisterUserServiceServer(server, &UserServiceImpl{})
    server.Serve(lis)
}

// Client (connection pooling automatic)
conn, _ := grpc.Dial("userservice:50051", 
    grpc.WithTransportCredentials(creds))
client := pb.NewUserServiceClient(conn)

for i := 0; i < 1000; i++ {
    user, _ := client.GetUser(ctx, &pb.UserID{Id: int32(i)})
    // All 1000 calls multiplexed on single HTTP/2 connection
}

gRPC Streaming

// Server-side streaming (e.g., downloading a large file)
func (s *FileServiceImpl) Download(req *pb.FileID, 
    stream pb.FileService_DownloadServer) error {
    // Stream chunks to client
    for chunk := range readChunks(file) {
        stream.Send(&pb.FileChunk{Data: chunk})
    }
    return nil
}

// Bidirectional streaming (e.g., websocket-like)
func (s *ChatServiceImpl) Stream(stream pb.ChatService_StreamServer) error {
    for {
        msg, _ := stream.Recv()  // Receive from client
        // Process
        stream.Send(&pb.Message{})  // Send to client
    }
}

Part 3: Service Mesh Internals — Istio and mTLS

The Problem Service Mesh Solves

Microservices architecture (20 services):
  Order → Payment → Auth
  Order → Inventory → DB
  Order → Shipping → Logistics

Current state:
  - No encryption between services (plaintext)
  - No authentication (just IP-based)
  - Timeouts scattered across 20 services
  - Retries scattered across 20 services
  - Load balancing per service (20 different configs)
  - Observability: logging only

Desired state:
  - All service-to-service encrypted (mTLS)
  - Mutual authentication (service knows caller is legitimate)
  - Centralized policies (timeouts, retries, rate limiting)
  - Centralized load balancing and failover
  - Centralized observability (trace every call)

A service mesh moves all of this from application code to infrastructure (sidecar proxies).

Architecture: Istio

┌─────────────────────────────────────────────────┐
│ Kubernetes Cluster                              │
├─────────────────────────────────────────────────┤
│                                                 │
│  Pod: order-service                             │
│  ├─ order container (your code)                │
│  └─ istio-proxy sidecar (Envoy)                │
│     └─ Intercepts all inbound/outbound TCP     │
│                                                 │
│  Pod: payment-service                           │
│  ├─ payment container                          │
│  └─ istio-proxy sidecar (Envoy)                │
│                                                 │
│  Control plane (istiod):                       │
│  ├─ Watches Kubernetes resources               │
│  ├─ Computes routing policies                  │
│  └─ Pushes config to all Envoy sidecars        │
│                                                 │
└─────────────────────────────────────────────────┘

How traffic flows:

1. order-container wants to call payment
2. order-container makes request to localhost:9080 (envoy)
3. Envoy sidecar intercepts:
   - Adds mTLS encryption
   - Validates payment service certificate
   - Applies retry policy (3x on 5xx)
   - Applies timeout (5s)
   - Selects backend using load balancing
   - Sends to payment-service:8080
4. payment-proxy (Envoy) receives, verifies mTLS
5. Forwards to payment-container:8080
6. Response flows back through both proxies

sequenceDiagram
    participant OC as order-container
    participant OE as order-proxy (Envoy)
    participant PE as payment-proxy (Envoy)
    participant PC as payment-container

    OC->>OE: request to localhost:9080
    OE->>OE: mTLS encrypt, apply retry (3x on 5xx) + timeout (5s), pick backend
    OE->>PE: mTLS request to payment-service:8080
    PE->>PE: verify mTLS, validate order-service certificate
    PE->>PC: forward to payment-container:8080
    PC-->>PE: response
    PE-->>OE: response (mTLS)
    OE-->>OC: response

mTLS in Istio

# Enable mTLS for entire namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT  # Require mTLS for all connections

# Now:
# - Every service gets auto-generated certificate (from cert controller)
# - Every sidecar enforces mTLS on inbound
# - Every sidecar uses mTLS on outbound
# - Certificate rotation: automatic every 24 hours

Certificate chain:

CA (Istio built-in or external)
  └─ Intermediate CA
     └─ Service cert (order-service)
     └─ Service cert (payment-service)

Each service cert:
  - Issued by Istio
  - Valid for 24 hours
  - Auto-rotated before expiry
  - Subject: spiffe://cluster.local/ns/production/sa/order-service
     (SPIFFE = Secure Production Identity Framework)

flowchart TB
    CA["Root CA\n(Istio built-in or external)"] --> IC["Intermediate CA"]
    IC --> OS["order-service cert\nspiffe://cluster.local/ns/production/sa/order-service"]
    IC --> PS["payment-service cert\nspiffe://cluster.local/ns/production/sa/payment-service"]
sequenceDiagram
    participant OE as order-proxy (Envoy)
    participant PE as payment-proxy (Envoy)

    OE->>PE: ClientHello (proposed ciphers, order-service cert)
    PE->>PE: verify order-service cert against trusted CA chain
    PE-->>OE: ServerHello + payment-service cert
    OE->>OE: verify payment-service cert against trusted CA chain
    OE->>PE: key exchange, Finished
    PE-->>OE: Finished
    Note over OE,PE: mutual trust established — both sides proved identity via SPIFFE-scoped certs

Istio VirtualService and DestinationRule

# Define endpoints (backends)
apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  selector:
    app: payment
  ports:
    - port: 8080

# Istio: Load balancing policy
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: payment
spec:
  host: payment-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        http2MaxRequests: 100
        maxRequestsPerConnection: 2
    loadBalancer:
      simple: LEAST_REQUEST  # Load balance by active connections
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s  # Eject backend after 5 errors

# Route traffic with retry policy
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: payment
spec:
  hosts:
    - payment-service
  http:
    - match:
        - uri:
            prefix: /api/v1
      route:
        - destination:
            host: payment-service
            port:
              number: 8080
      timeout: 5s
      retries:
        attempts: 3
        perTryTimeout: 1s
      fault:  # Chaos testing
        delay:
          percentage: 10  # 10% get 5s delay
          fixedDelay: 5s

Istio Observability

# Kiali: Mesh visualization
#   Shows service topology
#   Shows traffic flow, error rates, latency
#   
# Jaeger: Distributed tracing
#   Envoy sidecar propagates trace context
#   Every hop (order → payment → auth) logged with latency
#   
# Prometheus: Metrics
#   Envoy exposes metrics per service pair
#   request_duration_seconds{source="order",dest="payment"}
#   request_errors_total{source="order",dest="payment",status="500"}

Istio Overhead

✓ Benefits: - mTLS everywhere (no app code changes) - Centralized retry/timeout policy - Circuit breaker without code - Observability without instrumentation

✗ Costs: - Sidecar memory: ~50-100 MB per pod (1000 pods = 50-100 GB) - Latency: Envoy adds ~1-2 ms per hop - CPU: Proxying, mTLS handshakes (cryptography is expensive) - Operational complexity: Istio is notoriously complex to debug


Part 4: Linkerd — The Lightweight Mesh

Linkerd is a simpler alternative to Istio, focusing on reliability without the complexity.

Istio:     Feature-rich, complex, mature
Linkerd:   Minimal, lightweight, easy to operate

Istio data plane:   Envoy (C++, 30MB+, complex)
Linkerd data plane: Linkerd-proxy (Rust, 10MB, simple)

Istio CRDs:   30+ (VirtualService, DestinationRule, Gateway, etc.)
Linkerd:      3 CRDs (ServiceProfile, TrafficPolicy, Authorization)

Linkerd strengths: - Easier to understand (not feature-parity with Istio) - Smaller footprint (proxy is Rust, optimized) - Faster to debug (simpler config model) - Auto-mTLS with zero configuration

Linkerd setup:

# Install
linkerd install | kubectl apply -f -

# Inject sidecars into namespace
kubectl annotate namespace production linkerd.io/inject=enabled

# Define retries
apiVersion: linkerd.io/v1beta1
kind: ServiceProfile
metadata:
  name: payment
spec:
  service:
    name: payment
    namespace: production
  routes:
    - name: POST /api/charges
      condition:
        method: POST
        pathRegex: /api/charges
      isRetryable: true
      timeout: 5s

Part 5: Modern Congestion Control

BBR vs CUBIC

CUBIC (default in most Linux):

On packet loss:
  Multiplicative decrease is **0.7** (W_new = W_old * 0.7), not 0.8

Implication:
  Loss detected → immediate throughput drop → slow recovery

Problem: Induces queue buildup
  Window stays high → queue fills
  Loss happens → whole queue is lost

BBR (Google):

Instead of window size, track:
  Bandwidth: Max bandwidth observed
  RTT: Min RTT observed

pacing_rate = bandwidth × 1.25 (slight overshoot to find new bandwidth)

On packet loss:
  Reduce inflight by 15% (gradual)
  Reprobe bandwidth (don't get stuck)

Advantage:
  Targets inflight ≈ **BDP + a small queue** (not "2–3 packets")
  Faster recovery (reprobe continuously)
  Better for high-BDP networks (satellite, geo-distributed)

When BBR helps:

Scenario: 1 Gbps network, 100 ms RTT, BDP = 12.5 MB

CUBIC:
  Window opens to (and past) 12.5 MB
  Queue fills to several MB
  Latency skyrockets to 500+ ms
  Loss → window × 0.7 → slow recovery

BBR:
  Measures bandwidth (1 Gbps) and min RTT
  Keeps inflight ≈ BDP + small standing queue
  Latency stays near the min RTT
  Reprobe continuously for higher bandwidth

Enable BBR:

# /etc/sysctl.conf
net.ipv4.tcp_congestion_control=bbr
net.ipv4.tcp_notsent_lowat=16384
net.core.default_qdisc=fq

# Apply
sysctl -p


Part 6: Zero-Trust Networking

Traditional: "Trust the internal network."

Modern: "Never trust. Always authenticate and encrypt."

Old model:
  Internal network (VPN) → trusted
  Any service on VPN → can talk to any other

New model (zero-trust):
  No implicit trust
  Every connection: verify identity (mTLS)
  Every connection: verify authorization (policy)
  Every connection: encrypted

Implementation in Kubernetes

# Default: deny all traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

# Explicit allow: order → payment
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-order-to-payment
spec:
  podSelector:
    matchLabels:
      app: payment
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: order
      ports:
        - protocol: TCP
          port: 8080

SPIFFE/SVID for Service Identity

SPIFFE = Secure Production Identity Framework for Everyone

Every service gets an identity:
  URI: spiffe://cluster.local/ns/production/sa/order-service

Certificate (SVID = SPIFFE Verifiable Identity Document):
  Subject: spiffe://cluster.local/ns/production/sa/order-service
  Issued by: Cluster's CA
  Valid for: 24 hours (auto-rotated)

When order-service calls payment-service:
  1. order-service presents SPIFFE cert
  2. payment-service verifies it's from order-service
  3. Policy check: is order-service allowed to call payment?
  4. Call allowed or rejected

Part 7: Network Observability with eBPF

eBPF = extended Berkeley Packet Filter — run custom code in the kernel without recompiling it.

Traditional Network Observability

tcpdump: Capture packets (expensive, can drop packets)
ss/netstat: Socket stats (coarse, can miss events)
iptables rules: Log dropped packets (expensive)
Application logs: "We called this service" (high latency)

Problem: Low visibility into kernel, races in measurement

eBPF Tools

Cilium (networking)

Attach eBPF program to kernel network stack
  On every packet:
    - Identify source service (by IP)
    - Identify dest service (by IP/port)
    - Extract connection metadata
    - Forward to userspace for aggregation

Result:
  Service topology map (who calls whom)
  Traffic matrix (bytes/packets per service-pair)
  Latency distribution (TCP handshake, RTT)
  Zero overhead (eBPF runs in kernel)

Hubble (Cilium observability)

Metrics:
  connections_total{source_ns=production,source_app=order,
                     dest_ns=production,dest_app=payment}
  connection_duration_seconds{source_app=order,dest_app=payment}
  packets_dropped_total{reason=policy_denied}

Visibility:
  Real-time flow graph (order → payment, payment → db)
  Packet-level retransmit detection (TCP fast retransmit)
  Connection establishment latency (TCP handshake time)
  All without app instrumentation

Falco (runtime security via eBPF)

Monitor system calls:
  execve() → detect unexpected processes
  open() → detect unexpected file access
  connect() → detect unexpected network connections

Alert on anomalies:
  Process running in pod that shouldn't
  Container writing to unexpected files
  Service making outbound connection it shouldn't


Interview Questions

Q: What's the advantage of HTTP/3 over HTTP/2?

"HTTP/3 uses QUIC (UDP-based) instead of TCP. Key advantage: no TCP head-of-line blocking. In HTTP/2, one lost TCP packet stalls all streams. In HTTP/3, each stream is independent; one lost packet only affects that stream. First connection is 1-RTT; 0-RTT is resumption and is replay-sensitive. Connection migration: switch networks, the QUIC connection persists."

Q: When should you use gRPC instead of REST?

"gRPC for internal service-to-service (you control both ends). Benefits: smaller payload (protobuf vs JSON), faster serialization, HTTP/2 multiplexing. REST for public APIs or browser clients. Serialization savings are microseconds per call — they add CPU at high RPS; they are not a 100ms latency gift on each RPC."

Q: Explain how Istio's mTLS works and why you'd use it.

"Istio injects Envoy sidecars into every pod. Sidecar intercepts all traffic. On outbound: adds mTLS encryption, validates destination cert, applies retry/timeout policies. On inbound: accepts mTLS, validates caller cert, enforces authorization policies. Why: (1) mTLS without app code changes, (2) centralized retry/timeout/circuit breaker policies, (3) encrypted all service-to-service traffic, (4) SPIFFE identity (every service has cert proving who it is). Cost: ~50-100 MB memory per pod, ~1-2 ms latency per hop, operational complexity."

Q: Would you use Istio or Linkerd? Why?

"Linkerd for simplicity and ease of operation. Lighter weight (Rust proxy vs Envoy), fewer CRDs, auto-mTLS with minimal config, easier to debug. Istio if you need advanced features: traffic mirroring, A/B testing, multi-cluster management. At small-to-medium scale (<100 services), Linkerd's simplicity wins. At large scale with complex routing, Istio's power wins."

Q: Design a zero-trust network for a SaaS platform with 50 microservices, multi-region (US, EU), 10k RPS.

"I'd implement: (1) Cilium for networking (eBPF-based, zero-overhead observability), (2) SPIFFE/SVID for service identity (every service gets auto-rotated cert), (3) Linkerd for data plane (mTLS, retry, timeout policies, low operational burden), (4) NetworkPolicy for explicit allow-lists (default deny, add allow rules per service pair), (5) Observability via Hubble (service topology, latency, drop metrics from kernel, no app changes). Architecture: single Kubernetes cluster per region, Linkerd handles per-region mTLS, federation between clusters for multi-region (eventual consistency acceptable). Cost: Linkerd ~1% CPU overhead per service, memory ~50MB, Cilium adds ~100MB kernel memory. Security: every connection authenticated + encrypted, policy violations logged and alerted."


Key Takeaways

Remember

  1. HTTP/3 (QUIC): No TCP HOL, 1-RTT first handshake, 0-RTT resumption (replay-sensitive), connection migration. Matters for mobile and packet-loss networks.
  2. gRPC: 4-10× smaller payload, 10× faster serialization, HTTP/2 multiplexing. Use for internal service-to-service at scale.
  3. Service mesh: Moves networking concerns (mTLS, retries, timeouts, circuit breakers) from app code to infrastructure (Envoy/Linkerd sidecars).
  4. Istio: Feature-rich (30+ CRDs), complex, powerful. For large-scale microservices with advanced routing.
  5. Linkerd: Lightweight, simple, easy to debug. Rust-based proxy, minimal CRDs. Better at small-medium scale.
  6. mTLS in production: SPIFFE identity, auto-rotated certs, zero-trust networking (default deny + explicit allow).
  7. Congestion control: BBR better than CUBIC for high-latency/high-BW networks (satellite, geo-distributed). Keeps queues small.
  8. eBPF observability: Zero-overhead network visibility (Cilium/Hubble) without app instrumentation.
  9. Zero-trust: Every connection authenticated (mTLS), authorized (NetworkPolicy), encrypted. No implicit trust.
  10. Trade-offs: Service mesh adds complexity, memory, and latency. Worth it at 20+ services with complex routing needs.

Previous: Cloud Load Balancers