Modern And Distributed Networking
Service-to-Service Communication
cannot fail can time out, be refused,
JrCodex·10 min read
Jr Codex Computer Networks Notes
Level: Advanced Prerequisites: Chapter 3: Cloud Networking Time to complete: ~20 minutes
Table of Contents
- The Network Becomes the Bottleneck
- gRPC
- Timeouts, Retries and Backoff
- Circuit Breakers
- Service Discovery and the Mesh
- Distributed Tracing
- Summary & Next Steps
1. The Network Becomes the Bottleneck
What Changes With Microservices
─────────────────────────────────────────
A function call becomes a NETWORK CALL.
IN-PROCESS NETWORK
~1 nanosecond ~1 millisecond (same zone)
cannot fail can time out, be refused,
or silently vanish
no serialisation encode, transmit, decode
one failure mode many
─────────────────────────────────────────
THE FALLACIES OF DISTRIBUTED COMPUTING
─────────────────────────────────────────
Assumptions that are false, and that every
distributed system eventually violates:
1. the network is reliable
2. latency is zero
3. bandwidth is infinite
4. the network is secure
5. topology does not change
6. there is one administrator
7. transport cost is zero
8. the network is homogeneous
Every module of this curriculum has demonstrated
one of these to be false. This chapter is about
designing as though you believe that.
─────────────────────────────────────────
The Latency Arithmetic
─────────────────────────────────────────
One user request fanning out to 10 services, each
1 ms:
SEQUENTIAL 10 ms, plus 10 failure
opportunities
PARALLEL ~1 ms, but bounded by the SLOWEST
And with 10 services each 99.9% available, the
end-to-end availability of a sequential chain is
0.999^10 ≈ 99.0%.
Ten nines-and-a-bit services compose into
something noticeably worse. Composition
MULTIPLIES failure probability, which is the
central difficulty.
─────────────────────────────────────────
2. gRPC
What It Is
─────────────────────────────────────────
RPC over HTTP/2 (Module 5, Chapter 3) with
Protocol Buffers for serialisation.
You define the service in a .proto file, and
generate typed client and server code for any
language.
─────────────────────────────────────────
syntax = "proto3";
package orders;
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order);
rpc ListOrders (ListOrdersRequest) returns (stream Order); // server streaming
rpc WatchOrders (stream WatchRequest) returns (stream Order); // bidirectional
}
message GetOrderRequest { string order_id = 1; }
message Order {
string id = 1;
string customer = 2;
int64 total_cents = 3;
Status status = 4;
enum Status { PENDING = 0; PAID = 1; SHIPPED = 2; }
}Why It Suits Service-to-Service
─────────────────────────────────────────
BINARY AND COMPACT protobuf is far smaller than
JSON, and faster to parse
SCHEMA-FIRST the contract is explicit,
versioned and machine-checked
— no arguing about response
shapes
CODE GENERATION typed clients in every
language; a field rename
breaks at COMPILE time, not
in production
STREAMING four modes: unary, server
stream, client stream,
bidirectional
── over HTTP/2's multiplexed
streams
HTTP/2 MULTIPLEXING many concurrent calls on one
connection, avoiding
connection churn
─────────────────────────────────────────
Where REST Is Still Better
─────────────────────────────────────────
✗ BROWSER CLIENTS — gRPC needs grpc-web and a
proxy; the browser cannot speak raw gRPC
✗ PUBLIC APIs — external developers expect JSON
over HTTP and existing tooling
✗ DEBUGGING — you cannot curl a binary protocol
or read it in a proxy log
✗ SIMPLE CRUD — the schema and codegen machinery
is not worth it
THE USUAL SPLIT: REST or GraphQL at the edge for
clients; gRPC between internal services.
─────────────────────────────────────────
3. Timeouts, Retries and Backoff
THE MOST IMPORTANT RULE
─────────────────────────────────────────
EVERY network call needs a TIMEOUT.
A call without one waits forever. Under load,
every thread or connection ends up blocked on the
same slow dependency, and a service that is
merely SLOW takes down everything calling it.
This is the single most common cause of
cascading failure in distributed systems.
─────────────────────────────────────────
Setting Timeouts Sensibly
─────────────────────────────────────────
Base them on the p99 latency, not the average.
Roughly 2-3× p99.
BUDGET them across a call chain. If the user's
request has 1,000 ms, and A calls B calls C:
A's budget 1000 ms
B's budget 700 ms (A keeps time to respond)
C's budget 400 ms
Pass the REMAINING budget as a deadline in the
request. gRPC does this natively.
Without budgeting, inner calls happily consume
more time than the outer caller will ever wait
for — work that is discarded the moment it
completes.
─────────────────────────────────────────
import random, time
def retry(fn, attempts=3, base=0.05, cap=2.0, retryable=(TimeoutError, ConnectionError)):
"""Exponential backoff with FULL JITTER."""
for i in range(attempts):
try:
return fn()
except retryable:
if i == attempts - 1:
raise
backoff = min(cap, base * (2 ** i))
time.sleep(random.uniform(0, backoff)) # ← FULL jitter, not backoff/2RETRY ONLY WHAT IS SAFE
─────────────────────────────────────────
RETRY timeouts, connection failures, 502,
503, 504 — and only for IDEMPOTENT
operations (Module 5, Chapter 2)
DO NOT RETRY 400, 401, 403, 404, 422 — they will
fail identically forever, and you
burn budget and latency
For non-idempotent operations, use an IDEMPOTENCY
KEY so the retry is safe (DBMS Notes, Module 7,
Chapter 5).
─────────────────────────────────────────
WHY JITTER, SPECIFICALLY
─────────────────────────────────────────
Without it, every client that failed at the same
moment retries at the SAME moment.
The service recovers, is immediately hit by a
synchronised wave, fails again, and the cycle
repeats. A THUNDERING HERD that prevents recovery.
Random jitter spreads the retries. It is one line
of code and it is the difference between a
service recovering and a service oscillating.
It is the same insight as Ethernet's backoff
(Module 2, Chapter 3).
─────────────────────────────────────────
RETRY AMPLIFICATION
─────────────────────────────────────────
Retries at EVERY layer multiply.
3 retries at the gateway
× 3 at service A
× 3 at service B
= 27 requests reaching the struggling service C
Your retry logic is now a denial of service
attack on your own infrastructure.
RULE: retry at ONE layer, usually the outermost.
Or use a retry BUDGET — cap retries at a
percentage of total requests, so they cannot
dominate under widespread failure.
─────────────────────────────────────────
4. Circuit Breakers
The Idea
─────────────────────────────────────────
When a dependency is failing, STOP CALLING IT.
Failing fast is better than waiting for timeouts
— it frees your threads, and it gives the
struggling service room to recover.
─────────────────────────────────────────
The Three States
─────────────────────────────────────────
CLOSED normal. Requests pass. Failures are
counted.
│ failure rate exceeds the threshold
▼
OPEN requests FAIL IMMEDIATELY without
being attempted.
│ after a cooldown
▼
HALF-OPEN allow a FEW trial requests.
succeed ──► CLOSED
fail ──► OPEN again
─────────────────────────────────────────
import time
class CircuitBreaker:
def __init__(self, threshold=0.5, window=20, cooldown=30, trial=3):
self.threshold, self.window = threshold, window
self.cooldown, self.trial = cooldown, trial
self.results, self.state, self.opened_at, self.trials = [], "closed", None, 0
def allow(self):
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.opened_at >= self.cooldown:
self.state, self.trials = "half_open", 0 # try a few
return True
return False # FAIL FAST
return self.trials < self.trial
def record(self, ok):
if self.state == "half_open":
self.trials += 1
if not ok:
self.state, self.opened_at = "open", time.time()
elif self.trials >= self.trial:
self.state, self.results = "closed", []
return
self.results.append(ok)
self.results = self.results[-self.window:]
if len(self.results) >= self.window:
failure_rate = 1 - sum(self.results) / len(self.results)
if failure_rate >= self.threshold:
self.state, self.opened_at = "open", time.time()Always Have a Fallback
─────────────────────────────────────────
An open circuit means the call fails
IMMEDIATELY. What then?
- serve STALE cached data
- return a degraded response — hide the
recommendations, show the page
- queue the work for later
- return a clear error for this feature only
A circuit breaker without a fallback converts a
slow failure into a fast failure. Useful, and not
the point. The point is to keep serving what you
still can — Module 6's degrade rather than
collapse.
─────────────────────────────────────────
5. Service Discovery and the Mesh
The Problem
─────────────────────────────────────────
Instances are ephemeral. They autoscale, get
replaced, change addresses. Hardcoding an IP is
not an option (Chapter 3).
SERVICE DISCOVERY answers "where is service B
right now?"
Two approaches:
CLIENT-SIDE the client queries a registry and
picks an instance itself
SERVER-SIDE the client calls a stable name;
a load balancer resolves it
(Chapter 1)
─────────────────────────────────────────
THE SERVICE MESH
─────────────────────────────────────────
Every service instance gets a SIDECAR PROXY.
All traffic goes through it.
service A ──► sidecar ══► sidecar ──► service B
└── the mesh data plane ──┘
The sidecars handle, uniformly and outside
application code:
- service discovery and load balancing
- retries, timeouts, circuit breaking
(Sections 3-4)
- mTLS between services (Module 6, Chapter 3)
- metrics, logs and traces
- traffic splitting for canary deploys
─────────────────────────────────────────
The Honest Trade
─────────────────────────────────────────
GAIN consistent behaviour across every
language, with no library to update in
twelve codebases; mTLS everywhere without
touching application code
COST an extra network hop and process per
instance (latency and memory)
a substantial new system to operate and
debug
another failure mode
WHEN IT IS WORTH IT
Many services, several languages, and a real
need for uniform policy and mTLS.
WHEN IT IS NOT
Under roughly ten services, or one language.
A shared library is simpler and adequate.
A service mesh is infrastructure for a problem
you should confirm you have.
─────────────────────────────────────────
6. Distributed Tracing
The Problem It Solves
─────────────────────────────────────────
A user request touches twelve services. It is
slow. WHERE?
Each service's logs show only its own part, with
no way to connect them. Without tracing, you are
correlating timestamps by hand across twelve log
streams.
─────────────────────────────────────────
The Mechanism
─────────────────────────────────────────
TRACE ID one id for the whole request,
generated at the edge
SPAN ID one per operation
PARENT which span this one is nested inside
The ids are PROPAGATED in headers across every
hop:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736
-00f067aa0ba902b7-01
└ trace id ──┘ └ span id ┘
A backend reconstructs the tree and shows exactly
where the time went.
─────────────────────────────────────────
from opentelemetry import trace
from opentelemetry.propagate import inject, extract
tracer = trace.get_tracer(__name__)
def handle_request(incoming_headers):
ctx = extract(incoming_headers) # CONTINUE the trace
with tracer.start_as_current_span("handle_order", context=ctx) as span:
span.set_attribute("order.id", order_id)
outgoing = {}
inject(outgoing) # PROPAGATE to the next service
response = http_client.get(inventory_url, headers=outgoing)
return responseWhat a Trace Tells You Immediately
─────────────────────────────────────────
──────────────── request 840 ms ────────────────
gateway ▇▇ 20 ms
auth ▇ 10 ms
order-svc ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 790 ms
inventory ▇▇ 15 ms
pricing ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 750 ms ◄──
currency-api ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 740 ms ◄──
notify ▇ 5 ms
The problem is an external currency API called
from pricing. Ninety seconds of reading, not a
day of correlating logs.
IF YOU BUILD ONE THING FOR A DISTRIBUTED SYSTEM,
BUILD TRACING. Everything else is guesswork
without it.
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- Composing services multiplies failure probability, so ten 99.9% services in a chain give roughly 99% end-to-end availability.
- Every network call needs a timeout, budgeted across the chain and passed as a deadline — a missing timeout is the most common cause of cascading failure.
- Retries need jitter to avoid synchronised waves, and must happen at one layer only, or they multiply into a self-inflicted denial of service.
- A circuit breaker without a fallback only converts slow failure into fast failure; the value is in continuing to serve what you still can.
Module 7 Complete — What's Next
You now understand the infrastructure between a user and an application. Module 8 turns all seven modules into a practical skill: diagnosing a real network problem methodically, and building something end to end.
Concept Check
- Why does ten sequential 99.9%-available services give substantially worse end-to-end availability?
- Explain retry amplification, and give two ways to prevent it.
- Why is a service mesh not automatically the right choice for a system with six services?
Next Module
→ Module 8: Practice and Capstone
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Computer Networks Index