31 — Reference tables
These tables collect the track’s main choices in one place. Each row is a summary, and the linked chapter has the reasoning, the caveats and the measurements behind it. Product behavior changes between versions, so the product-specific rows describe the current documentation, and are worth rechecking before a decision depends on them.
Data stores by access pattern
The first question is the access pattern, and the product comes second (chapter 6).
| Model | Fits | Fits poorly | Examples |
|---|---|---|---|
| Relational | Varied and changing queries, joins, constraints, multi-row transactions, money | Write rates beyond one primary, unless partitioned or sharded | PostgreSQL |
| Key-value | Get and put by key at high rates: sessions, carts, counters, caches | Any query that isn’t by key | Redis, DynamoDB |
| Document | Whole aggregates read and written together, records whose shape varies | Joins across documents, many-to-many relationships | MongoDB, or PostgreSQL jsonb |
| Wide-column | Very high write rates, reads by a known partition key and a range inside it | Queries not planned into the table design, ad hoc filtering | Cassandra |
| Graph | Multi-hop traversals run constantly: networks, dependencies, recommendations | Bulk scans and aggregates | Neo4j |
| Search | Full-text search, relevance ranking, facets | Being the only copy of the data, strict read-after-write | Elasticsearch |
| Time-series | Appends by time, range scans, downsampling, retention by age | Updates to old points, high-cardinality labels | Prometheus, or PostgreSQL partitioned by time |
| Object storage | Large immutable blobs: media, backups, logs, data lake files | Small frequent updates, queries inside objects, multi-object atomicity | Amazon S3 and compatible stores |
Scaling and guarantees, product by product
| Product | Scales reads by | Scales writes by | Transactions | Read consistency |
|---|---|---|---|---|
| PostgreSQL | Hot standby replicas that answer read-only queries | A bigger primary, batching; declarative partitioning splits a table on one server; extensions such as Citus shard across nodes | Any set of statements; default isolation is Read Committed, with Repeatable Read and Serializable available | Primary is current; the documentation calls standby data eventually consistent with the primary |
| Redis | Replicas | Redis Cluster, which spreads 16,384 hash slots across primaries | MULTI/EXEC runs queued commands without other clients interleaving, with no rollback; scripts are also transactional; in a cluster, multi-key operations need keys in one hash slot | Replication is asynchronous by default; WAIT reduces but doesn’t remove the chance of losing acknowledged writes on failover |
| DynamoDB | Partitions chosen by a hash of the partition key, managed by the service | Same, with partitions added automatically as throughput or storage grows | TransactWriteItems: up to 100 actions, 4 MB in total, same account and Region | Eventually consistent by default; strongly consistent reads optional on tables and local secondary indexes, never on global secondary indexes |
| MongoDB | Replica set secondaries | Sharded clusters | Single-document operations are atomic; multi-document transactions need feature compatibility version 4.0 on a replica set and 4.2 on a sharded cluster | Set per operation by read concern: local by default, with majority, linearizable and others available |
| Cassandra | Adding nodes; more replicas | Adding nodes | Lightweight transactions: compare-and-set within a single partition; no cross-partition transactions, per its architecture documentation | Tunable per query: ONE, QUORUM, LOCAL_QUORUM, ALL |
| Elasticsearch | Replica shards | Primary shards | None across documents | Near real-time: refreshed every second by default, on indices searched in the last 30 seconds |
| Prometheus | A single server’s local storage; larger setups use external storage through the remote read and write APIs | Same | None | Local storage isn’t clustered or replicated, and the documentation says to manage it like any other single-node database |
| Amazon S3 | Managed by the service | Managed by the service | None across keys; updates to a single key are atomic | Strong read-after-write for PUT and DELETE; bucket configuration is eventually consistent |
Notes on these rows:
- PostgreSQL read replicas can return stale data, and long queries on a standby can be cancelled when they conflict with changes being replayed (chapter 12).
- DynamoDB transactions cost two underlying reads or writes per item, one to prepare and one to commit, and conflicting transactions are cancelled. Transactions apply only within the Region where they’re called. In global tables, the default multi-Region mode is eventually consistent, and replicas in other Regions can see a transaction partly applied while it replicates.
- MongoDB’s documentation says a distributed transaction usually costs more than single-document writes, and that for many cases an embedded data model remains the better choice.
- Cassandra quorums overlap when the write and read replica counts satisfy , so
QUORUMwrites followed byQUORUMreads see the latest acknowledged write. - Redis Cluster is best-effort about write safety: its specification says there are usually small windows where acknowledged writes can be lost.
- Redis durability depends on persistence settings. With
appendfsync everysec, the default for the append-only file, a crash can lose about one second of writes. RDB snapshots alone can lose minutes. - S3 doesn’t lock objects for concurrent writers. If two
PUTrequests to one key overlap, the one with the latest timestamp wins.
Scaling patterns by bottleneck
A scaling change helps only if it targets the resource that’s actually running out (chapter 2, chapter 4).
| Bottleneck | Symptom | Fix | Cost | Chapter |
|---|---|---|---|---|
| Application CPU | High CPU on app servers, latency rising with load | Stateless servers behind a load balancer, more instances | State has to move out of the servers | 5 |
| Distance to users | High latency for far users, with fast server times | CDN, regional deployments | Cache invalidation, data placement | 1, 5 |
| Connection setup | Many new connections, handshake time dominates | Keep-alive, connection pools, HTTP/2 or HTTP/3 | Long-lived connections to manage | 1, 2 |
| Database reads | Read latency and database CPU rising together | Cache, read replicas, better indexes | Staleness, invalidation, replica lag | 6, 7, 12 |
| Database connections | Pool timeouts while the database itself is idle | A connection pooler, shorter transactions, fewer slow queries | One more component | 6 |
| Database writes | Write latency, commit waits, I/O saturation | Batching, group commit, asynchronous work, then partitioning | Latency from batching; partitioning is hard to undo | 2, 8, 11 |
| Data volume | Storage growth, slow maintenance, long backups | Partition by time, archive to object storage, retention, compression | Queries across tiers cost more | 11, 14 |
| Hot key | One cache node or shard far busier than the rest | Local caching, replicating the key, splitting it into sub-keys | Staleness, more complex reads | 7, 11 |
| Slow work in the request path | Requests waiting on email, images, reports | A queue and background workers | Eventual completion, retries, duplicates | 8 |
| Continuous event processing | Batch jobs falling behind, stale aggregates | Stream processing with windows and checkpoints | State management, late data | 9 |
| Load spikes | Brief overload, then recovery | Queues as buffers, autoscaling, load shedding | Delay, or rejected requests | 10, 16 |
| Fan-out tail latency | p99 of a page far above the p99 of each backend | Fewer calls in series, hedged requests, timeouts | Extra load from hedging | 10 |
| Coordination | Lock waits, contention on a single row or leader | Partition the contended state, avoid global locks | Weaker ordering across partitions | 11, 13 |
Consistency at a glance
| Model | Guarantee | Typical source |
|---|---|---|
| Linearizable | Each operation appears to happen at one instant, in real-time order | A single leader read from the leader; consensus systems |
| Sequential | All nodes see one order, not necessarily real-time | Replicated state machines |
| Causal | Causally related operations appear in order | Systems that track dependencies |
| Read-your-writes | A client sees its own updates | Reading from the leader after a write, or session tokens |
| Monotonic reads | A client never sees older data after newer | Pinning a client to one replica |
| Eventual | Replicas converge if writes stop | Asynchronous replicas, caches, search indexes |
Chapter 12 covers each model. The defaults of common components:
| Component | Default behavior | Stronger option |
|---|---|---|
| PostgreSQL primary | Read Committed transactions | Repeatable Read or Serializable isolation |
| PostgreSQL hot standby | Eventually consistent with the primary | Synchronous standbys with synchronous_commit = remote_apply, or reading from the primary |
| DynamoDB table read | Eventually consistent | ConsistentRead on tables and local secondary indexes |
| DynamoDB global table | Multi-Region eventual consistency | Multi-Region strong consistency mode |
| Cassandra | Set per query | QUORUM reads and writes |
| Redis replica | Asynchronous | WAIT for a number of acknowledgements, which still isn’t strong consistency |
| Amazon S3 object | Strong read-after-write | Already strong for single keys |
| Elasticsearch | Visible after the next refresh, about a second | An explicit call to the refresh API |
| Cache in front of a database | As stale as the TTL or the invalidation path allows | Delete on write, leases, short TTLs (chapter 7) |
Delivery guarantees at a glance
| Guarantee | Meaning | How it’s achieved |
|---|---|---|
| At most once | Messages may be lost, but are never redelivered | Send once, never retry |
| At least once | Messages are never lost, but may be redelivered | Retry until acknowledged |
| Exactly once | The effect happens once | At-least-once delivery plus idempotent processing, or transactions within one system |
Chapter 15 explains why exactly-once only holds within specific boundaries. What common systems provide:
| System | Delivery | Ordering | Notes |
|---|---|---|---|
| Amazon SQS standard queue | At least once | Best effort | Consumers must handle duplicates |
| Amazon SQS FIFO queue | Exactly-once processing within the queue, using deduplication IDs | In order within each message group | Lower throughput than standard queues |
| Kafka producer | Idempotent by default (enable.idempotence is true, acks is all) | In order within a partition | Transactions across partitions since 0.11.0.0 |
| Kafka consumer | Depends on when offsets are committed | In order within a partition | Default isolation is read_uncommitted, so aborted transactional messages are visible unless set to read_committed |
| Kafka to an external system | At least once, unless the sink cooperates | Per partition | Kafka’s documentation says exactly-once to other systems generally requires their cooperation |
| Stream processors with checkpoints | Exactly-once state, with transactional or idempotent sinks | Per key or partition | Chapter 9 |
| HTTP API call with retries | At least once | None | Idempotency keys make retries safe (chapter 15) |
With Kafka, acks=all waits for the current in-sync replicas, not every assigned replica. Setting min.insync.replicas makes the partition refuse writes when too few replicas are in sync, trading availability for durability.
Failure modes and mitigations
| Failure | What happens | Mitigations | Chapter |
|---|---|---|---|
| A server crashes | Its in-flight requests fail; in-memory state is lost | Several stateless instances, health checks, retries of idempotent requests | 5, 16 |
| A zone or datacenter is lost | A share of capacity disappears at once | Spread across zones, headroom to absorb the loss | 3, 4 |
| A dependency is slow | Threads and connections fill up waiting; the slowness spreads | Timeouts, bulkheads, circuit breakers | 16 |
| A retry storm | Retries multiply load on a struggling service | Exponential backoff with jitter, retry budgets | 16 |
| Overload | Latency rises sharply, then requests time out | Load shedding, backpressure, rate limiting | 10, 16, 22 |
| A metastable failure | The system stays down after the trigger is gone | Shed load hard, drain queues, reduce retries | 16 |
| A cache stampede | A hot key expires and many requests hit the database at once | Request coalescing, leases, jittered TTLs, serving stale | 7 |
| A cold cache | After a restart, all reads go to the database | Warming, gradual traffic return, a database sized for misses | 4, 7 |
| A hot key or shard | One node is overloaded while others are idle | Better partition keys, splitting the key, local caching | 11 |
| Replica lag | Reads return old data; a user doesn’t see their own write | Read-your-writes routing, monitoring lag | 12 |
| Failover with asynchronous replication | Acknowledged writes that hadn’t replicated are lost | Synchronous or quorum replication for critical data | 12 |
| Split brain | Two nodes both act as leader | Consensus, quorums, fencing tokens | 13 |
| Clock skew | Timestamps order events wrongly; leases misbehave | Logical clocks, bounded lease times, fencing | 13 |
| A dual write | A database and a queue disagree after a partial failure | Transactional outbox, change data capture | 15 |
| A poison message | One bad message blocks or crashes consumers repeatedly | Retry limits, dead-letter queues | 8 |
| A queue backlog | Work piles up faster than it’s processed | More consumers, backpressure, alerting on age of oldest message | 8 |
| Disk full or flush misconfigured | Writes fail, or acknowledged data is lost on power failure | Capacity alerts, retention, verified durable flushes | 2, 14 |
| A bad deployment or configuration change | Many instances fail together | Canary releases, gradual rollout, fast rollback | 20 |
| An expired certificate | TLS handshakes fail for every client | Automated renewal, expiry alerts | 1, 19 |
| DNS changes propagate slowly | Clients keep using an old address until their cache expires | Short TTLs ahead of planned moves, failover below DNS | 1, 5 |
| Connection pool exhaustion | Requests wait for a connection while the database is idle | Shorter transactions, timeouts on acquiring a connection, fixing slow queries | 6 |
Numbers from chapter 4
The full table, with how each number was obtained, is in chapter 4. Measured numbers come from one laptop and differ on other hardware.
Latencies and rates
| Quantity | Value | Kind |
|---|---|---|
| Read from level-1 cache | About 1 ns | Measured |
| Random read from main memory | About 110 ns | Measured |
| Loopback TCP round trip | 16 µs | Measured |
| Random 4 KB SSD read, one at a time | 77 µs median | Measured |
| Random 4 KB SSD reads, 64 in parallel | 130,000 a second | Measured |
| Durable flush | 2.9 ms median | Measured |
| Internet round trip | About 9 ms near, 160 ms far | Measured |
| Trivial HTTP requests on one core | About 136,000 a second | Measured |
| TCP setup | 1 round trip | Sourced (RFC 9293) |
| TLS 1.3 setup | 1 round trip | Sourced (RFC 8446) |
| TLS 1.2 setup | 2 round trips | Measured, chapter 1 |
| Light in a vacuum | About 300 km per ms | Sourced (SI definition) |
Sizes and limits
| Quantity | Value | Kind |
|---|---|---|
| TCP initial window | 10 segments, at most 14,600 bytes | Sourced (RFC 6928) |
| PostgreSQL page | 8 kB by default | Sourced |
| PostgreSQL row overhead | 23-byte header plus 4-byte item identifier | Sourced |
PostgreSQL default max_connections | Typically 100 | Sourced |
| UUID | 16 bytes | Sourced (RFC 9562) |
| DynamoDB item | At most 400 KB | Sourced |
| DynamoDB transaction | Up to 100 actions, 4 MB | Sourced |
Formulas
| Quantity | Formula |
|---|---|
| Average rate | Requests a day ÷ 86,400 |
| Peak factor from the busiest hour’s share | |
| Storage | Records a day × bytes × days × (1 + index share) × copies |
| Egress | Peak read rate × response size; × 8 for bits |
| Database reads behind a cache | Read rate × (1 − hit ratio) |
| Servers | Peak ÷ (capacity per server × target utilization), rounded up |
| Concurrency | , then margin for peaks |
Cache hit ratios under Zipf traffic
From the simulation in chapter 4, one million items, an LRU cache:
| Share of items cached | s = 0.8 | s = 1.0 | s = 1.2 |
|---|---|---|---|
| 1% | 23.2% | 58.6% | 87.5% |
| 10% | 48.7% | 77.9% | 95.0% |
| 20% | 60.6% | 84.2% | 96.3% |
The takeaway
Choose a store from the access pattern, and a scaling change from the resource that’s actually running out. Check the consistency and delivery guarantees of each component against what the data needs, because the defaults are usually weaker than the strongest option. Every failure mode in the table has a known mitigation, and every mitigation has a cost, which is why the chapters behind these tables matter more than the tables themselves.