Client-Server Model
Request-Response
Read aloud is unavailable in this browser.
The Fundamental Communication Pattern
At the heart of the client-server model lies a deceptively simple interaction: the client asks, the server answers. This request-response pattern is the fundamental communication paradigm that governs how clients and servers exchange information. Every HTTP call, every database query, every API invocation follows this pattern—a client formulates a request expressing what it wants, transmits it to a server, and waits for a response that fulfills (or rejects) that request.
Understanding request-response deeply means understanding how networked applications communicate. This pattern isn't merely a convention; it's the logical structure that enables loosely-coupled, scalable distributed systems.
By the end of this page, you will understand the complete anatomy of request-response communication: how requests are structured, how responses are formulated, synchronous versus asynchronous patterns, protocol-level message formatting, error handling semantics, and the variations and extensions of this fundamental pattern used in modern systems.
The Request-Response Model Explained
The request-response model is a communication pattern where:
- The client initiates by sending a request message to the server
- The server processes the request, performing whatever operations are required
- The server responds by sending a response message back to the client
- The client processes the response, acting on the information received
This is a pull-based model—the client pulls data from the server when it wants it. The server doesn't push data unprompted; it only responds when asked.
Key Characteristics:
| Characteristic | Description | Implication |
|---|---|---|
| Initiated by Client | Server never sends unsolicited responses | Server design simplified; client controls timing |
| Paired Messages | Each request expects exactly one response | Enables reliable transaction semantics |
| Synchronous Nature | Client typically waits for response | Simple programming model but potential latency |
| Stateless Possible | Each pair can be independent | Enables horizontal scaling and simplicity |
Why This Pattern Dominates:
The request-response pattern is nearly universal in networked computing for good reasons:
-
Simplicity — Easy to understand, implement, and debug. The cause-effect relationship is clear.
-
Reliability — The client knows whether its request succeeded based on the response (or timeout).
-
Scalability — Stateless request-response enables load balancing across multiple servers.
-
Interoperability — Standard request/response protocols (HTTP, SQL, etc.) enable heterogeneous systems to communicate.
Request-response isn't the only communication pattern. Streaming (server pushes data continuously), publish-subscribe (clients subscribe to events), and bidirectional streaming (both sides send independently) serve different needs. However, request-response remains the most common pattern and often underlies other patterns as a building block.
Anatomy of a Request
A well-formed request must communicate several pieces of information to the server. While specific protocols vary, most requests share common structural components.
- •Operation/Method — What action the client wants performed: GET (retrieve), POST (create), DELETE (remove), QUERY (search). Defines the semantics of the request.
- •Resource Identifier — What entity the operation applies to: a URL path, a table name, a file path, an object ID. Locates the target of the operation.
- •Headers/Metadata — Ancillary information about the request: authentication credentials, content type, client capabilities, caching directives, correlation IDs for tracing.
- •Body/Payload — The data being sent: form data, JSON object, binary file, query parameters. Not all requests have bodies (e.g., GET typically has no body).
- •Protocol Version — Which version of the protocol is being used, enabling backward compatibility.
HTTP Request Example:
12345678910111213
POST /api/v2/users HTTP/1.1Host: api.example.comContent-Type: application/jsonAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Accept: application/jsonX-Request-ID: 550e8400-e29b-41d4-a716-446655440000Content-Length: 89 { "username": "johndoe", "email": "john.doe@example.com", "role": "developer"}Breakdown of the HTTP Request:
| Component | Example | Purpose |
|---|---|---|
| Request Line | POST /api/v2/users HTTP/1.1 | Method, resource path, protocol version |
| Host Header | Host: api.example.com | Target server (enables virtual hosting) |
| Content-Type | Content-Type: application/json | Body format for parsing |
| Authorization | Authorization: Bearer ... | Authentication credentials |
| Accept | Accept: application/json | Preferred response format |
| X-Request-ID | X-Request-ID: 550e... | Request correlation for tracing |
| Content-Length | Content-Length: 89 | Body size in bytes |
| Body | {"username": ...} | The actual data being submitted |
Request Semantics by Method:
HTTP methods define the intent of the request:
| Method | Semantic | Idempotent? | Safe? | Has Body? |
|---|---|---|---|---|
| GET | Retrieve resource | Yes | Yes | No |
| POST | Create resource / Submit data | No | No | Yes |
| PUT | Replace resource entirely | Yes | No | Yes |
| PATCH | Modify resource partially | No* | No | Yes |
| DELETE | Remove resource | Yes | No | Rarely |
| HEAD | GET but response headers only | Yes | Yes | No |
| OPTIONS | Query server capabilities | Yes | Yes | Optional |
- Idempotent: Multiple identical requests have the same effect as one request
- Safe: Request doesn't modify server state (read-only)
*PATCH is contentiously idempotent depending on patch semantics
Anatomy of a Response
Responses communicate the result of the server's processing—whether the request succeeded, failed, or requires further action. Like requests, responses have a defined structure.
- •Status Code — A numeric code indicating the result category: 2xx (success), 3xx (redirect), 4xx (client error), 5xx (server error).
- •Status Text — Human-readable description of the status code (e.g., 'OK', 'Not Found', 'Internal Server Error').
- •Headers/Metadata — Information about the response: content type, caching directives, server information, cookies to set, rate limit status.
- •Body/Payload — The response data: requested resource, created entity, error details, empty for some responses.
- •Protocol Version — Echo of protocol version for compatibility.
HTTP Response Example:
123456789101112131415161718
HTTP/1.1 201 CreatedContent-Type: application/jsonLocation: /api/v2/users/12345X-Request-ID: 550e8400-e29b-41d4-a716-446655440000Cache-Control: no-cacheDate: Sat, 17 Jan 2026 06:00:00 GMTContent-Length: 156 { "id": "12345", "username": "johndoe", "email": "john.doe@example.com", "role": "developer", "createdAt": "2026-01-17T06:00:00Z", "links": { "self": "/api/v2/users/12345" }}HTTP Status Code Categories:
| Range | Category | Meaning | Common Codes |
|---|---|---|---|
| 1xx | Informational | Request received, processing | 100 Continue, 101 Switching Protocols |
| 2xx | Success | Request successfully processed | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | Further action needed | 301 Moved Permanently, 302 Found, 304 Not Modified |
| 4xx | Client Error | Request was invalid | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 5xx | Server Error | Server failed to process | 500 Internal Server Error, 503 Service Unavailable |
Critical Status Codes to Know:
| Code | Name | When to Use | Client Action |
|---|---|---|---|
| 200 | OK | Request succeeded; response has body | Process response body |
| 201 | Created | Resource created successfully | Use Location header for new resource |
| 204 | No Content | Success but no response body | No body to process |
| 301 | Moved Permanently | Resource URL changed permanently | Update bookmarks; follow Location |
| 304 | Not Modified | Cached version is current | Use cached response |
| 400 | Bad Request | Malformed request syntax | Fix request and retry |
| 401 | Unauthorized | Authentication required | Authenticate and retry |
| 403 | Forbidden | Authenticated but not authorized | Request different action/resource |
| 404 | Not Found | Resource doesn't exist | Check URL; don't retry |
| 429 | Too Many Requests | Rate limit exceeded | Back off; check Retry-After |
| 500 | Internal Server Error | Server had an error | Retry with backoff |
| 503 | Service Unavailable | Server temporarily overloaded | Retry after delay |
Synchronous Request-Response
In synchronous request-response, the client sends a request and blocks until it receives the response. This is the most intuitive and common pattern—the client waits for the answer before proceeding.
Characteristics of Synchronous Pattern:
| Aspect | Description |
|---|---|
| Blocking | Client thread waits; cannot do other work during the wait |
| Simple Mental Model | Call-and-return semantics familiar from function calls |
| Latency-Sensitive | Total time = network RTT + server processing time |
| Resource Consumption | One thread tied up per pending request |
| Error Handling | Straightforward—errors return to the call site |
When Synchronous Works Well:
- Operations that complete quickly (< 100ms)
- Simple command-line tools and scripts
- Operations where the caller truly has nothing else to do
- Sequential operations where each step depends on the previous
When Synchronous Becomes Problematic:
- High-latency operations (network calls across continents, complex computations)
- Need to make multiple independent requests (sequential is slow)
- Interactive applications where blocking freezes the UI
- Servers handling many clients (thread-per-request doesn't scale)
12345678910111213141516171819
// Synchronous HTTP client callfunction fetchUserData(userId): url = "https://api.example.com/users/" + userId // This call BLOCKS until response arrives response = http.get(url) // Thread waits here // Execution only continues after response if response.status == 200: return response.json() else if response.status == 404: throw UserNotFoundError(userId) else: throw ApiError(response.status, response.body) // Usage - sequential and blockinguser = fetchUserData("12345") // Blocks hereorders = fetchUserOrders(user.id) // Blocks here tooprint(user.name + " has " + orders.count + " orders")Synchronous calls without timeouts can block forever if the server becomes unresponsive. Always configure connection timeouts (how long to wait for connection establishment), read timeouts (how long to wait for response data), and total request timeouts. Unbounded waits are a common source of production outages.
Asynchronous Request-Response
In asynchronous request-response, the client sends a request and then continues executing without waiting for the response. The response is handled when it arrives, typically via callbacks, promises, or async/await constructs.
Asynchronous Programming Models:
1. Callbacks
The traditional approach—pass a function to be called when the response arrives:
fetchUser(userId, (error, user) => {
if (error) { handleError(error); return; }
fetchOrders(user.id, (error, orders) => {
// Nested callback - "callback hell"
});
});
Problem: Deeply nested callbacks become unreadable ("callback hell").
2. Promises
Represent a future value that may resolve or reject:
fetchUser(userId)
.then(user => fetchOrders(user.id))
.then(orders => displayOrders(orders))
.catch(error => handleError(error));
Better: Chainable, single error handler, but still somewhat awkward.
3. Async/Await
Syntactic sugar making async code look synchronous:
async function loadData(userId) {
try {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
displayOrders(orders);
} catch (error) {
handleError(error);
}
}
Best: Reads like synchronous code but executes asynchronously.
123456789101112131415161718192021222324252627282930313233343536373839
// Pattern: Making independent parallel requestsasync function fetchDashboardData(userId) { // These requests run IN PARALLEL - huge time savings const [user, orders, notifications, analytics] = await Promise.all([ fetchUser(userId), fetchOrders(userId), fetchNotifications(userId), fetchAnalytics(userId) ]); // All responses available here return { user, orders, notifications, analytics };} // Timing comparison:// Sequential: 100ms + 100ms + 100ms + 100ms = 400ms total// Parallel: max(100ms, 100ms, 100ms, 100ms) = 100ms total // Pattern: Racing requests (first to respond wins)async function fetchWithFallback(userId) { const result = await Promise.race([ fetchFromPrimary(userId), fetchFromSecondary(userId) ]); return result; // Whichever responds first} // Pattern: Request with timeoutasync function fetchWithTimeout(url, timeout) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); return response; } finally { clearTimeout(timeoutId); }}| Aspect | Synchronous | Asynchronous |
|---|---|---|
| Thread during wait | Blocked, idle | Free for other work |
| Code complexity | Simple, linear | More complex control flow |
| Parallel requests | Sequential only | Easily parallelized |
| UI responsiveness | Freezes during wait | Remains responsive |
| Error handling | Try/catch at call site | Promise rejection / callback error |
| Resource efficiency | Thread-per-request | Many requests per thread |
| Debugging | Straightforward stack traces | Async stack traces can be confusing |
Request-Response Across Protocols
The request-response pattern is implemented differently across various protocols, each optimizing for its specific use case. Understanding these variations reveals the universality and flexibility of the pattern.
| Protocol | Request Format | Response Format | Special Features |
|---|---|---|---|
| HTTP/1.1 | Text-based (method + headers + body) | Status line + headers + body | One request per connection at a time |
| HTTP/2 | Binary frames, HEADERS + DATA | Binary frames, HEADERS + DATA | Multiplexed (many concurrent on one connection) |
| HTTP/3 | QUIC streams, binary frames | QUIC streams, binary frames | Independent streams, 0-RTT possible |
| DNS | Query message (12B header + question) | Response message (header + RRs) | Usually UDP, very compact |
| SQL (PostgreSQL) | Query string + parameters | Row data + metadata | Pipelined queries, cursor-based results |
| gRPC | Protobuf-encoded RPC call | Protobuf-encoded response | Streaming, bidirectional, strongly typed |
| SMTP | MAIL FROM, RCPT TO, DATA commands | Status codes (250, 354, etc.) | Multi-command transaction |
| Redis | RESP command (e.g., GET key) | RESP value (String, Array, etc.) | Pipelining, Pub/Sub modes |
HTTP/2 Multiplexing: A Key Evolution
HTTP/1.1 has a fundamental limitation: only one request-response exchange can be in flight per connection at a time (head-of-line blocking). To make multiple simultaneous requests, clients open multiple connections—wasteful and limited.
HTTP/2 introduced multiplexing: many requests and responses can be interleaved on a single connection, each identified by a stream ID:
Connection:
Stream 1: Request A headers
Stream 3: Request B headers
Stream 1: Request A data
Stream 3: Request B data
Stream 1: Response A headers + data
Stream 3: Response B headers
Stream 3: Response B data
This transforms the request-response model from strictly sequential to highly concurrent over a single TCP connection, dramatically improving efficiency for web pages with many resources.
12345678910111213141516171819202122232425262728
// DNS Request-Response Example CLIENT REQUEST (Query):----------------------------------------Transaction ID: 0xABCDFlags: Standard query (QR=0)Questions: 1Question Section: Name: www.example.com Type: A (IPv4 address) Class: IN (Internet) SERVER RESPONSE:----------------------------------------Transaction ID: 0xABCD (matches request)Flags: Response (QR=1), Authoritative (AA=1)Questions: 1Answers: 1Question Section: Name: www.example.com Type: A Class: INAnswer Section: Name: www.example.com Type: A Class: IN TTL: 300 seconds Data: 93.184.216.34Protocols like DNS use transaction IDs to match responses to requests. This is essential when multiple requests may be outstanding simultaneously (especially over UDP where there's no connection state). The client generates a unique ID, includes it in the request, and the server echoes it in the response.
Error Handling in Request-Response
Robust request-response handling must account for the many ways interactions can fail. Errors occur at every layer—network, transport, protocol, and application—and must be handled appropriately.
| Layer | Error Type | Example | Handling Strategy |
|---|---|---|---|
| Network | No connectivity | DNS failure, network unreachable | Fail fast, show offline indicator |
| Transport | Connection failure | Connection refused, connection reset | Retry with different server, exponential backoff |
| Transport | Timeout | No response received in time | Retry idempotent requests, show loading state |
| Protocol | Malformed response | Invalid HTTP, truncated data | Log error, possibly retry |
| Application | Client error (4xx) | Bad request, not found, forbidden | Don't retry; fix input or show error |
| Application | Server error (5xx) | Internal error, unavailable | Retry with backoff for transient failures |
Idempotency and Safe Retries:
The key question when an error occurs: is it safe to retry?
- Idempotent operations (GET, PUT, DELETE) can be safely retried—repeating them has the same effect as doing them once
- Non-idempotent operations (POST, some PATCH) may not be safely retriable—a retry might create duplicate data
Example Problem:
Client: POST /orders (create order)
Server: Processes request, creates order, starts sending response
Network: Connection drops before response reaches client
Client: Sees timeout error - did the order get created?
If retry: might create duplicate order
If don't retry: might have lost the order
Solutions:
-
Idempotency Keys: Client sends unique key with request; server deduplicates:
POST /orders Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 -
Request IDs for Correlation: Include ID in request, check if already processed before retry
-
Two-Phase Operations: First request reserves/stages; second request confirms
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
async function requestWithRetry(url, options, config = {}) { const { maxRetries = 3, baseDelay = 1000, // 1 second maxDelay = 30000, // 30 seconds retryableStatuses = [429, 500, 502, 503, 504] } = config; let attempt = 0; let lastError; while (attempt <= maxRetries) { try { const response = await fetch(url, options); if (response.ok) { return response; } // Don't retry client errors (except rate limiting) if (response.status >= 400 && response.status < 500 && response.status !== 429) { throw new ClientError(response.status, await response.text()); } // Check if this status is retryable if (!retryableStatuses.includes(response.status)) { throw new ServerError(response.status); } lastError = new ServerError(response.status); } catch (error) { // Network errors are generally retryable if (!isNetworkError(error)) throw error; lastError = error; } // Calculate delay with exponential backoff + jitter const delay = Math.min( baseDelay * Math.pow(2, attempt) + Math.random() * 1000, maxDelay ); console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`); await sleep(delay); attempt++; } throw new MaxRetriesExceeded(lastError);}KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.