Request-Response
The foundational synchronous communication pattern where a caller blocks waiting for a direct reply — covering HTTP/gRPC mechanics, timeout strategies, connection pooling, safe retries, and circuit breaking.
What it is
Request-response is the simplest and most ubiquitous inter-service communication model: a client sends a request and blocks until it receives a response or a timeout fires. The protocol can be HTTP/1.1, HTTP/2 (used by gRPC), WebSockets (for server-push variants), or raw TCP. The key characteristic is temporal coupling — the caller and the callee must both be alive and reachable at the same moment in time. Every browser page load, every REST API call, and every gRPC unary RPC is a request-response interaction. Despite the rise of asynchronous patterns, synchronous request-response remains the right default for user-facing reads and simple, low-latency queries because it offers the simplest mental model: you ask a question and you get the answer.
Over HTTP/1.1 the connection is typically kept alive via Connection: keep-alive; HTTP/2 multiplexes many logical streams over a single TCP connection, eliminating head-of-line blocking at the HTTP layer. gRPC builds on HTTP/2 and adds a strongly typed contract via Protocol Buffers, binary framing, and built-in streaming variants (server-streaming, client-streaming, bidirectional). REST over HTTP/1.1 is still dominant for public APIs due to tooling familiarity and firewall compatibility.
Why it exists
Many operations are inherently interrogative: "What is the current price?", "Is the user authorised?", "Does this record exist?" These questions have no useful answer unless they are answered now. Asynchronous patterns require the caller to manage correlation state and poll or listen for a reply, which adds complexity without benefit when the latency of the operation is small and predictable. Request-response maps naturally to programming language idioms: a function call, an await, a future — the runtime suspends the caller until the answer arrives, keeping application code straightforward. It also aligns with HTTP's stateless, cacheable semantics, allowing intermediate caches (CDN, reverse proxy) to serve responses without ever reaching the origin service.
When to use
- User-facing reads where the browser or mobile app is waiting for data to render — every millisecond of latency is visible to the user.
- Simple, low-latency queries — fetching a user profile, looking up a product by SKU, checking inventory count.
- Authentication and authorisation checks where the request cannot proceed until a yes/no answer is obtained.
- Synchronous validation — credit card number format check, address geocoding before saving an order.
- Admin and operational APIs — triggering a one-off job, querying a health endpoint, reading configuration.
- When simplicity matters more than scale — early-stage products where operational overhead of async messaging is unjustified.
When not to use
- Long-running operations (> a few seconds) — HTTP timeouts and user patience do not accommodate multi-minute workflows; use async job patterns instead.
- Fan-out to many downstream services — calling 10 services serially to assemble a composite response compounds latency and failure probability.
- Fire-and-forget notifications — if the caller does not need the result, sync coupling adds unnecessary latency and failure surface.
- High-throughput write ingestion — a broker queue absorbs bursts far better than a synchronous endpoint that blocks a thread per connection.
- Cross-datacenter critical paths — inter-region RTT of 60–100 ms can make synchronous call chains unusable under P99 SLOs.
Typical architecture
A client (browser, mobile app, or upstream service) opens an HTTP/2 connection to the service. Connection pooling ensures the TCP handshake cost is amortised across many requests. A reverse proxy or API gateway sits in front to handle TLS termination, rate limiting, and routing. Downstream the service may make further synchronous calls, forming a call chain. Each hop in the chain must budget its latency share to meet the overall SLO. A circuit breaker wraps each downstream call to fail fast when the dependency is degraded.
Client
│
│ HTTP/2 (TLS)
▼
API Gateway ──── Rate Limit / Auth
│
│ gRPC (internal mTLS)
▼
Service A ──── local cache (Redis, in-proc)
│ \
│ └─ GET /prices (sync)
│ │
│ Service B [circuit breaker]
│
└─ GET /inventory (sync)
│
Service C [circuit breaker]
Timeouts: Gateway→A = 500 ms
A→B = 200 ms
A→C = 150 ms
Pros and cons
Pros
- Simplest mental model — caller code reads as a function call.
- Immediate feedback — errors and validation failures surface instantly.
- HTTP caching (ETags, Cache-Control) reduces origin load for reads.
- Mature tooling — load balancers, service meshes, APM agents all understand HTTP natively.
- Easy to trace end-to-end with distributed tracing (W3C trace context propagated in headers).
Cons
- Temporal coupling — caller and callee must both be available simultaneously.
- Cascading failures — a slow downstream stalls the caller thread/goroutine/connection.
- Latency amplification in deep call chains — P99 latency compounds multiplicatively.
- Thread/connection exhaustion under high load — each in-flight request occupies a resource until response or timeout.
- Retry storms — naive retry logic can overload an already-struggling service.
Implementation notes
Timeouts: Always set two timeouts — a connect timeout (fail fast if the remote is unreachable, typically 100–200 ms) and a read/request timeout (bound the total call duration, sized to your SLO). In gRPC, use per-call deadlines propagated via context so the entire call chain respects the same absolute deadline, not a fresh relative timeout per hop.
Connection pooling: HTTP/1.1 clients must use a pool with a maximum-connections-per-host setting. HTTP/2 clients multiplex over a single connection, but still need a pool per host to handle TLS session establishment. Pool size should be tuned to max_concurrent_requests / avg_latency_s using Little's Law. Too few connections queue requests; too many overwhelm the server.
Idempotency for safe retries: GET and HEAD requests are safe (no side effects) and idempotent by specification — retry freely on network errors. PUT and DELETE are idempotent by definition. POST is neither safe nor idempotent — introduce an Idempotency-Key header (a UUID generated by the caller) so the server can detect and suppress duplicate submissions within a deduplication window (typically 24 h stored in Redis).
Retry policy: Apply exponential backoff with jitter — delay = min(cap, base * 2^attempt) + random(0, jitter). Only retry on transient errors (503, 429, connection reset, timeout). Never retry on 400, 401, 403, 422 — these are deterministic failures that retrying cannot fix.
Circuit breaker: Wrap each downstream call with a circuit breaker (Hystrix, Resilience4j, Envoy). When the failure rate exceeds a threshold (e.g., 50% over a 10-second window) the circuit opens, and subsequent calls fail immediately without attempting the downstream. This prevents thread exhaustion and gives the dependency time to recover. The circuit transitions to half-open after a sleep window to probe recovery.
Common failure modes
- Timeout cascade: An upstream service increases its timeout, masking a slow downstream and allowing threads to accumulate until the upstream exhausts its connection pool.
- Thundering herd on retry: Many clients timeout simultaneously and retry at the same moment, creating a retry storm that overwhelms the recovering service.
- Missing timeout propagation: Each service uses its own fixed timeout rather than the remaining budget from a parent deadline, causing deep chains to accept requests they cannot fulfill in time.
- Connection pool starvation: Pool is sized too small; requests queue behind in-flight calls and appear as high latency even though the downstream is healthy.
- Double charge on non-idempotent retry: Retrying a payment POST without an idempotency key results in duplicate charges.
- Circuit breaker not tuned: A circuit breaker with too-sensitive thresholds opens on normal error rates during deployments, causing unnecessary degradation.
Decision checklist
- Does the caller need the result before it can proceed? If not, consider async.
- Is the expected p99 latency of the downstream within the caller's latency budget?
- Have timeouts been set for every outbound call — both connect and read timeouts?
- Are retries limited to idempotent/safe operations, or is an idempotency key scheme in place for non-idempotent calls?
- Is a circuit breaker configured around each downstream dependency?
- Is the connection pool sized according to expected concurrency (Little's Law)?
- Are W3C trace context headers (
traceparent,tracestate) propagated to enable distributed tracing?
Example use cases
- E-commerce product page: Browser calls the BFF (Backend for Frontend), which calls the product service (gRPC, 80 ms budget) and inventory service (gRPC, 60 ms budget) in parallel, then assembles the page payload synchronously.
- Auth token validation: Every API gateway request calls a token introspection endpoint synchronously; result is cached per token with a short TTL to minimise latency.
- Real-time pricing: Trading dashboard hits a pricing microservice with <10 ms SLO; gRPC over HTTP/2 with a pre-warmed connection pool meets the target.
Related patterns
- Publish/Subscribe — async alternative when decoupling is more important than immediacy.
- Fan-Out / Fan-In — parallelises multiple downstream sync calls to stay within a single latency budget.
- Correlation IDs — essential for tracing a synchronous call chain across services.
- Circuit Breaker — wraps synchronous calls to prevent cascading failures.
Further reading
- gRPC Deadlines — gRPC official guide on propagating deadlines across services.
- Idempotency Keys — Stripe Engineering on designing safe retries for payment APIs.
- Timeouts, Retries, and Backoff with Jitter — AWS Builder's Library deep dive.
- CircuitBreaker — Martin Fowler's original pattern description.