Client-Server Model
Server Types
Read aloud is unavailable in this browser.
A Taxonomy of Servers
Not all servers are created equal. The simple concept of 'a program that responds to requests' encompasses an enormous variety of architectures, each optimized for different workloads, scale requirements, and operational constraints. From a tiny embedded server handling a few requests per hour to a global-scale service processing millions per second, the design decisions differ dramatically.
Understanding the taxonomy of server types enables architects and engineers to select the right architecture for their needs, understand the tradeoffs involved, and design systems that meet real-world requirements.
By the end of this page, you will understand the major categorizations of servers: iterative vs. concurrent, stateless vs. stateful, connection-oriented vs. connectionless, single-tier vs. multi-tier architectures. You'll learn when to use each type and how modern systems often combine multiple approaches.
Iterative vs. Concurrent Servers
The most fundamental distinction in server design is how multiple clients are handled: one at a time (iterative) or simultaneously (concurrent).
- •Sequential Processing — Handles one client completely before accepting the next
- •Single Thread/Process — No concurrency complexity; straightforward control flow
- •Blocking Accept — Server blocks on accept(), handles client, then blocks again
- •Low Overhead — No thread creation, context switching, or synchronization
- •Limited Throughput — Subsequent clients wait in queue while one is served
- •Simple Error Handling — Failure in one client doesn't affect infrastructure
- •Parallel Processing — Multiple clients served simultaneously
- •Multiple Threads/Processes/Async — Requires concurrency management
- •Non-blocking Accept — Main loop accepts; dispatches to workers
- •Higher Overhead — Context switching, memory per connection, synchronization
- •High Throughput — Clients don't wait for others; full resource utilization
- •Complex Error Handling — One client's failure must not crash the server
1234567891011121314151617181920212223242526272829303132
// ITERATIVE SERVERfunction iterativeServer(port): socket = listen(port) while true: client = socket.accept() // Block until client connects handleClient(client) // Block until request complete client.close() // Only then accept next client // Timeline with 3 clients (each takes 100ms):// Client 1: |===100ms===|// Client 2: wait |===100ms===|// Client 3: wait |===100ms===|// Total time: 300ms // CONCURRENT SERVER (Process-per-connection)function concurrentServer(port): socket = listen(port) while true: client = socket.accept() pid = fork() if pid == 0: // Child process handleClient(client) exit(0) else: // Parent process close(client) // Parent doesn't need client socket continue // Immediately accept next // Timeline with 3 clients (each takes 100ms):// Client 1: |===100ms===|// Client 2: |===100ms===|// Client 3: |===100ms===|// Total time: ~100ms (limited by slowest)| Scenario | Best Choice | Reason |
|---|---|---|
| Simple admin tool with guaranteed single user | Iterative | Simplicity; no concurrency bugs possible |
| Local development server | Iterative or simple concurrent | Low load; debugging easier without concurrency |
| DNS server | Concurrent (UDP) | Many short requests; connectionless suits iterative-per-datagram |
| Web server | Concurrent (thread pool/async) | Many simultaneous users; requests can be slow |
| Database server | Concurrent (connection pool) | Multiple applications querying simultaneously |
| Real-time game server | Concurrent (event-driven) | Many players; low latency critical |
Modern servers often use hybrid approaches. For example, a thread pool handles concurrent clients (concurrent), but each thread processes its assigned client's requests sequentially (iterative within that connection). The nginx architecture uses multiple worker processes (concurrent), each running an event loop that handles many connections (concurrent), but each connection's request is processed to completion before the next for that connection (iterative for ordering).
Stateless vs. Stateful Servers
Another critical distinction is whether the server maintains information about clients between requests. This decision has profound implications for scalability, reliability, and complexity.
Stateless Server:
A stateless server treats each request as independent—no memory of previous interactions. Every request contains all information needed to process it.
Client: GET /user/123 (with auth token)
Server: [Validates token, queries database, returns user]
[Immediately forgets this client]
Client: GET /user/123/orders (with auth token)
Server: [Validates token again, queries database, returns orders]
[No memory of previous request]
Stateful Server:
A stateful server maintains session information between requests. The client is 'remembered' across a series of interactions.
Client: CONNECT user/password
Server: [Validates, creates session 'abc123']
[Stores: session 'abc123' = {user: 'john', authenticated: true}]
Client: REQUEST data (session: abc123)
Server: [Finds session 'abc123', knows user is 'john']
[Returns data for 'john']
Client: QUIT (session: abc123)
Server: [Destroys session 'abc123']
- •Horizontal Scalability — Any server can handle any request; add servers freely behind load balancer
- •Simple Failover — If one server dies, others immediately handle traffic; no state to recover
- •No State Synchronization — Multiple servers don't need to share session data
- •Simpler Implementation — No session management code, cleanup, timeouts
- •Cache-Friendly — Identical requests always produce identical responses (for GET)
- •Predictable Behavior — Each request is independent; easier to test and debug
- •Reduced Request Size — Context carried in session, not every request
- •Efficient Protocol Flows — Multi-step transactions don't repeat setup
- •Caching of Expensive Computations — Session can cache user-specific data
- •Connection Efficiency — Database connections, TLS sessions reused
- •Natural for Some Protocols — FTP, IMAP, database connections inherently stateful
- •Real-time Features — WebSocket, game servers need persistent state
| Aspect | Stateless | Stateful |
|---|---|---|
| Scalability | Easily horizontally scalable | Requires session affinity or shared state |
| Reliability | Easy failover; any server works | Failover loses session unless persisted |
| Load Balancing | Any algorithm works | Sticky sessions or state sharing needed |
| Complexity | Simpler server; more complex client/request | More complex server; simpler requests |
| Memory Usage | Lower per-server memory | Memory grows with active sessions |
| Request Size | Larger (context in each request) | Smaller (context in session) |
| Examples | REST APIs, HTTP/1.1 (generally) | FTP, IMAP, database connections, WebSocket |
Making Stateful Systems Scalable:
When statefulness is required but scalability is also needed, several patterns help:
-
Session Affinity (Sticky Sessions) — Load balancer routes all requests from a user to the same server. Simple but creates uneven load and failover issues.
-
External Session Store — Move session state to a shared store (Redis, Memcached, database). Any server can access any session.
-
Client-Stored Sessions — Encode session in encrypted token (like JWT) sent with each request. Server is stateless but client carries state.
-
Sharded State — Partition sessions across servers deterministically (e.g., by user ID hash). Predictable routing without sticky sessions.
Most modern large-scale systems prefer stateless where possible, using pattern #2 or #3 when sessions are unavoidable.
Connection-Oriented vs. Connectionless Servers
Servers can be categorized by whether they use connection-oriented protocols (like TCP) or connectionless protocols (like UDP). This choice affects reliability, latency, and server design.
| Aspect | Connection-Oriented (TCP) | Connectionless (UDP) |
|---|---|---|
| Connection Setup | Three-way handshake before data | No setup; data sent immediately |
| State per Client | Socket per connection | Single socket for all clients |
| Reliability | Guaranteed delivery, ordering | Best-effort; may lose/reorder packets |
| Server Resources | Higher (memory per connection) | Lower (no per-client state) |
| Latency | Higher (handshake overhead) | Lower (no handshake) |
| Request Pattern | Stream of bytes | Individual datagrams |
| NAT/Firewall | Connection tracked | Harder to track; timeouts |
1234567891011121314151617181920212223242526272829303132
// TCP CONNECTION-ORIENTED SERVERfunction tcpServer(port): serverSocket = socket(SOCK_STREAM) bind(serverSocket, port) listen(serverSocket, backlog=128) while true: // Accept returns NEW socket for this specific client clientSocket = accept(serverSocket) // Can do multiple read/writes on this socket while data = read(clientSocket): response = process(data) write(clientSocket, response) close(clientSocket) // UDP CONNECTIONLESS SERVERfunction udpServer(port): serverSocket = socket(SOCK_DGRAM) bind(serverSocket, port) while true: // Receive datagram with client address (data, clientAddr) = recvfrom(serverSocket) response = process(data) // Send response back to that specific client sendto(serverSocket, response, clientAddr) // No connection state; next recvfrom could be different client- •Use TCP (Connection-Oriented) — When reliability is critical (web, email, file transfer, databases). When you need flow control and congestion control. When request/response are large or multi-part.
- •Use UDP (Connectionless) — When latency is more important than reliability (games, VoIP, DNS). When a single request fits in one datagram. When you can handle or tolerate occasional loss.
- •Modern Trend: QUIC — Combines UDP's low latency with reliability built above UDP (used by HTTP/3). Gets benefits of both without TCP's limitations.
UDP servers can handle enormous numbers of 'clients' because there's no per-client connection state. A single UDP socket can receive datagrams from millions of sources. This is why DNS servers can handle massive query rates. However, the server must implement any needed reliability (retransmission, ordering) at the application layer.
Single-Tier vs. Multi-Tier Architectures
As applications grow in complexity, servers are organized into multiple tiers, each handling different responsibilities. This separation enables specialized optimization, independent scaling, and clearer system organization.
Two-Tier Architecture (Client-Server):
The classic client-server model: clients connect directly to a server that handles everything.
┌──────────┐ ┌───────────────────────────────────┐
│ Client │ ───────▶│ Server │
│ (UI) │ ◀─────── │ - Presentation Logic │
└──────────┘ │ - Business Logic │
│ - Data Access │
│ - Database │
└───────────────────────────────────┘
- Advantages: Simple deployment, low latency within server
- Disadvantages: Monolithic, hard to scale, single point of failure
Three-Tier Architecture:
Separates presentation, business logic, and data storage.
┌──────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐
│ Client │────────▶│ Presentation│────────▶│ Business │────────▶│ Database │
│ │◀────────│ Tier │◀────────│ Logic Tier │◀────────│ Tier │
└──────────┘ │ (Web Server)│ │ (App Server)│ │ (DBMS) │
└─────────────┘ └─────────────┘ └──────────┘
Tier Responsibilities:
| Tier | Responsibility | Technology Examples |
|---|---|---|
| Presentation | Handle HTTP, SSL termination, static content, routing | nginx, Apache, load balancers |
| Business Logic | Process requests, implement business rules, orchestrate | Node.js, Java/Spring, Python/Django |
| Data | Persistent storage, transactions, queries | PostgreSQL, MongoDB, Redis |
N-Tier / Modern Service Architecture:
Modern systems often go beyond three tiers, with specialized components:
| Component Type | Purpose | Examples |
|---|---|---|
| Load Balancer | Distribute traffic, health checks | HAProxy, AWS ALB, nginx |
| API Gateway | Authentication, rate limiting, routing | Kong, AWS API Gateway, Envoy |
| Cache Layer | Reduce database load, speed up responses | Redis, Memcached, Varnish |
| Message Queue | Async processing, decoupling | Kafka, RabbitMQ, AWS SQS |
| Search Service | Full-text search, analytics | Elasticsearch, Algolia |
| CDN | Serve static content close to users | CloudFlare, AWS CloudFront |
| Background Workers | Async job processing | Celery, Sidekiq, custom |
Benefits of Multi-Tier:
- Independent Scaling: Scale each tier to its bottleneck
- Technology Freedom: Use best tool for each tier
- Isolation: Failure in one tier can be contained
- Team Organization: Teams can own specific tiers
- Security Boundaries: Database tier not directly exposed
Costs of Multi-Tier:
- Network Latency: Each tier hop adds latency
- Operational Complexity: More components to deploy, monitor, debug
- Distributed System Issues: Network partitions, consistency challenges
Specialized Server Types
Beyond the fundamental categorizations, many specialized server types exist to address specific use cases.
| Server Type | Purpose | Key Characteristics | Examples |
|---|---|---|---|
| Proxy Server | Intermediary for requests | Forward (client-side) or reverse (server-side); caching, filtering, load balancing | Squid, nginx, HAProxy |
| Caching Server | Store frequently accessed data | In-memory for speed; TTL-based expiration; invalidation strategies | Redis, Memcached, Varnish |
| WebSocket Server | Persistent bidirectional connections | Event-driven; pub/sub patterns; connection management | Socket.io, ws, Pusher |
| Streaming Server | Deliver media streams | Adaptive bitrate; buffering; real-time or on-demand | Wowza, nginx-rtmp, HLS servers |
| Game Server | Manage multiplayer game state | Low latency; UDP often; high tick rate; anti-cheat | Photon, custom engines |
| Edge Server | Content close to users | Geographically distributed; cache static content | CDN nodes (CloudFlare, Fastly) |
| Virtual Server | Multiple logical servers on one host | Isolation; resource sharing; management complexity | VPS providers, Docker containers |
| Embedded Server | Server in constrained environment | Minimal resources; specific protocols (CoAP, MQTT) | IoT devices, appliances |
Proxy Servers in Detail:
Proxy servers are particularly important as they sit between clients and origin servers, providing various benefits:
Forward Proxy:
[Client] ──▶ [Forward Proxy] ──▶ [Internet] ──▶ [Origin Server]
- Client explicitly configured to use proxy
- Used for: Access control, anonymity, caching, filtering content
- Examples: Corporate proxy, Tor network
Reverse Proxy:
[Client] ──▶ [Internet] ──▶ [Reverse Proxy] ──▶ [Origin Server(s)]
- Client unaware of proxy
- Used for: Load balancing, SSL termination, caching, security
- Examples: nginx in front of application servers
Transparent Proxy:
- Intercepts traffic without client configuration
- Used by ISPs for caching, enterprises for filtering
- May raise privacy/security concerns
In practice, a single server process often combines multiple roles. nginx can simultaneously serve static files (web server), reverse proxy to application servers, cache responses (caching server), and terminate SSL (security function). Understanding the conceptual roles helps even when they're combined in implementation.
Server Process Models
Beyond simple concurrent vs. iterative, modern servers employ sophisticated process and threading models to maximize performance and reliability.
Single-Process, Single-Threaded (Event Loop):
┌─────────────────────────────────────────┐
│ Single Process / Single Thread │
│ ┌─────────────────────────────────────┐ │
│ │ Event Loop │ │
│ │ - poll() for events │ │
│ │ - Process ready I/O │ │
│ │ - Run callbacks │ │
│ │ - Back to poll() │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
- Model: All I/O is non-blocking; single event loop handles thousands of connections
- Examples: Node.js (single process), Redis (single-threaded core)
- Pros: No synchronization needed; simple mental model for I/O-bound work
- Cons: Can't use multiple CPU cores; CPU-intensive work blocks everything
Multi-Process, Single-Threaded Each (Pre-fork):
┌─────────────────────────────────────────┐
│ Master Process (coordination) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │Worker │ │Worker │ │Worker │ │
│ │Process│ │Process│ │Process│ │
│ │(event │ │(event │ │(event │ │
│ │ loop) │ │ loop) │ │ loop) │ │
│ └───────┘ └───────┘ └───────┘ │
└─────────────────────────────────────────┘
- Model: Master spawns worker processes; each worker has its own event loop
- Examples: nginx, Gunicorn, PM2 cluster mode
- Pros: Uses all CPU cores; worker crash doesn't kill server; isolation
- Cons: Inter-process communication complex; memory not shared (unless explicit)
Multi-Process, Multi-Threaded:
┌─────────────────────────────────────────┐
│ Process Pool │
│ ┌─────────────────┐ ┌────────────────┐│
│ │ Process 1 │ │ Process 2 ││
│ │ ┌────┐ ┌────┐ │ │ ┌────┐ ┌────┐ ││
│ │ │ T1 │ │ T2 │ │ │ │ T1 │ │ T2 │ ││
│ │ └────┘ └────┘ │ │ └────┘ └────┘ ││
│ │ ┌────┐ ┌────┐ │ │ ┌────┐ ┌────┐ ││
│ │ │ T3 │ │ T4 │ │ │ │ T3 │ │ T4 │ ││
│ │ └────┘ └────┘ │ │ └────┘ └────┘ ││
│ └─────────────────┘ └────────────────┘│
└─────────────────────────────────────────┘
- Model: Multiple processes, each with multiple threads
- Examples: Apache with worker MPM, Java application servers
- Pros: Maximum parallelism; shared memory within process
- Cons: Most complex; debugging challenging; thread safety required
| Model | CPU Utilization | Memory Sharing | Fault Isolation | Complexity |
|---|---|---|---|---|
| Single process, single thread | One core only | All shared | None (crash kills all) | Lowest |
| Single process, multi-thread | All cores | All shared | Thread crash can kill process | Medium |
| Multi-process, single thread each | All cores | Explicit IPC only | Process crash isolated | Medium |
| Multi-process, multi-thread | All cores | Within process shared | Process crash isolated | Highest |
Choosing the Right Server Type
Selecting the appropriate server architecture depends on workload characteristics, scale requirements, reliability needs, and operational constraints.
- •Consider Concurrency Requirements — How many simultaneous clients? Tens (iterative may work), thousands (need concurrent), millions (event-driven async).
- •Analyze Request Characteristics — CPU-intensive (need threads)? I/O-intensive (event loop excels)? Mix (hybrid model)?
- •Evaluate State Requirements — Can you go stateless? If not, how will you handle session affinity or shared state?
- •Assess Reliability Needs — Mission-critical (multi-tier, redundancy)? Can tolerate occasional failures (simpler architecture)?
- •Consider Latency Sensitivity — Ultra-low latency (single tier, UDP)? Can tolerate some latency (multi-tier, caching okay)?
- •Factor in Operational Complexity — Who operates this? Small team (simpler architecture)? Dedicated SRE (can handle complex multi-tier)?
- •Plan for Growth — Expect 10x growth? Design for horizontal scaling from the start.
| Scenario | Recommended Architecture | Key Considerations |
|---|---|---|
| Simple REST API, low traffic | Single tier, thread pool or async | Keep simple; add tiers when needed |
| Web application, moderate traffic | Three tier (LB → App → DB) | Standard proven pattern; good starting point |
| Real-time chat/gaming | WebSocket server, event-driven | Optimize for connection density and latency |
| High-traffic content site | CDN + cache + origin (multi-tier) | Cache aggressively; tier for different content types |
| Machine learning API | Async with worker pool | Separate compute-intensive work from request handling |
| IoT data ingestion | Event-driven, stateless, UDP-capable | Optimize for many small messages |
| Financial trading | Ultra-low latency, minimal tiers | Every microsecond matters; simplify critical path |
Premature optimization of architecture is as dangerous as premature optimization of code. Start with the simplest architecture that could work, measure actual bottlenecks, and evolve. Many successful systems began as monoliths and were decomposed as scale demanded. Complexity has ongoing costs—add it only when justified by real requirements.
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.