Problem
Every database connection is an OS process. Every thread in your application is a kernel-scheduled entity with memory costs. Understanding the difference between processes, threads, and user-space threads explains the fundamental trade-offs in all concurrency models.
Why It Matters (Latency, Throughput, Cost)
PostgreSQL connection model: 1 client = 1 forked OS process
fork() cost: ~2ms (copy page tables for 50MB process)
Per-connection RAM: ~5â10MB (shared memory + stack)
1000 connections: ~10GB RAM on DB server alone
Thread model (application server):
Thread creation: ~50Ξs
Per-thread stack: 1â8MB (default)
Context switch: 1â10Ξs + TLB flush
Goroutine (Go):
Creation cost: ~200ns
Per-goroutine stack: 2KB (grows on demand)
"Context switch": ~100ns (user-space scheduler)Mental Model
Process:
âââââââââââââââââââââââââââââââââââââââââââ
â Virtual Address Space (unique per proc)â
â âââââââââââ âââââââââââ ââââââââââââ â
â â Text â â Data â â Heap â â
â â (code) â â(globals)â â(malloc'd)â â
â âââââââââââ âââââââââââ ââââââââââââ â
â âââââââââââââââââââââââââââââââââââ â
â â Thread 1 Stack (8MB) â â
â âââââââââââââââââââââââââââââââââââ â
â File descriptors, signal handlers, ... â
âââââââââââââââââââââââââââââââââââââââââââ
Thread (within a process):
Shares: heap, code, file descriptors
Own: stack, registers, thread-local storageOS Scheduler
The Linux CFS (Completely Fair Scheduler) uses a red-black tree of runnable tasks ordered by virtual runtime. At each scheduling point, it picks the task with the smallest virtual runtime.
Schedule() invoked when:
1. Timer interrupt (every ~4ms by default with HZ=250)
2. Syscall completes and returns to user space
3. Thread blocks on I/O (yields voluntarily)
4. Thread exits
Context switch steps:
1. Save current thread registers to kernel stack (RSP, RIP, general purpose)
2. Save FPU/SIMD state if dirty (~100 cycles)
3. Switch page tables if different process â TLB flush
4. Load new thread registers
5. Return to user spaceUnderlying Theory
fork() and copy-on-write: When PostgreSQL forks for a new connection, the child starts with a copy of the parent's page tables. Physical pages are shared until either process writes â then the kernel creates a private copy (COW fault, ~1Ξs per page). PostgreSQL's shared buffers (buffer pool) are mapped shared memory, not COW'd.
Process isolation: Separate address spaces mean a bug in one PostgreSQL backend cannot corrupt another backend's memory â important for a multi-tenant DB server. Threads within a process share memory â a corrupt pointer in one thread can crash all threads.
Complexity Analysis
| Operation | Cost | Notes |
|---|---|---|
| fork() | O(virtual_pages) | |
| pthread_create() | O(1) | |
| goroutine creation | O(1) | |
| Context switch (thread) | O(1) | |
| Context switch (goroutine) | O(1) |
Benchmark
10,000 concurrent clients:
Thread-per-client: 10,000 Ã 8MB stack = 80GB RAM â infeasible
Process-per-client: 10,000 Ã 10MB = 100GB RAM â infeasible
Goroutine-per-client: 10,000 Ã 2KB = 20MB RAM â trivial
Async coroutine-per: 10,000 Ã 2KB state = 20MB RAM â trivialKey Takeaways
- PostgreSQL uses one process per connection â this is why connection pools matter.
- OS threads cost 1â8MB each. Goroutines cost 2KB. This is why Go handles more connections.
- Context switches cost 1â10Ξs + TLB flush. At 10K threads switching every 1ms: significant CPU waste.
- fork() is cheap for small processes, expensive for large ones (PostgreSQL's 50MB is "large").
- Process isolation is a feature: bugs are contained. Thread-sharing is a feature for communication.
Related Modules
../04-scheduling.mdâ Linux CFS scheduler internals../02-memory-management.mdâ Virtual memory and COW../../07-core-backend-engineering/04-threading-vs-async-vs-event-loop.mdâ Applied to concurrency choices