NoSQL
NoSQL is the umbrella name for databases that do not store relational tables queried with SQL: key-value, document, wide-column and graph stores. Most of them place each record on a node by a key you choose, and that key decides which queries are cheap, which are expensive and which are impossible. The page opens with sixteen things a relational database did for you and what you do instead, then covers the four families, choosing the partition key, replicas and conflicts, modelling for access patterns, running one in production, five documented cases, and where it degrades.
- Four families
- Key-value, document, wide-column, graph
- Placement
- By a key you choose — and cannot cheaply change
- Current versions
- MongoDB 9.0 (Sep 2026); Cassandra 5.0 (6.0 in alpha); Valkey 9.1
Sixteen things a relational database did for you — and what you do instead
Nobody adopts a NoSQL store in order to lose features. Teams adopt one for scale, for a flexible record shape, or because the managed service is one click away. Then they discover, one at a time, the things the relational database had been doing without being asked. Here are sixteen of them: what the relational database did, what a partitioned store does instead, and the pattern that gets it back. Not every store loses all sixteen — MongoDB has had multi-document transactions since 2018 — but every store makes some of them cost more. Later sections cite the rows by number.
| # | The relational database did this | A partitioned store instead | What you do instead |
|---|---|---|---|
| 1 | Answered a question nobody thought of when the table was designed | A query without the partition key asks every node — or is refused | List the access patterns first; choose the key so the important ones carry it |
| 2 | Joined tables at read time | No joins across partitions; the application makes one more request per related item | Store the answer in the shape the screen reads; fetch the rest in one batch |
| 3 | Found rows by any indexed column, and the index was never behind | An index spanning partitions is updated asynchronously and can miss a write you just made | Global indexes for paths that tolerate a lag; read by key where it matters |
| 4 | Counted, summed and grouped: COUNT, SUM, GROUP BY | An aggregate scans everything, or does not exist | Maintain counters on write; send the rest to an analytical store |
| 5 | Sorted and paginated by any column | Order exists only inside one partition, by its sort key | Put the order you need into the sort key; paginate from the last key seen |
| 6 | Changed several rows in one transaction | Atomic per item; multi-item transactions, where they exist, cost more and have limits | Keep what changes together in one item; use the engine’s transaction for the rest, within its limits |
| 7 | Kept any column unique — one account per email | Uniqueness only on the primary key | A second item keyed by the unique value, written in the same transaction |
| 8 | Refused a reference to something that does not exist | Nothing checks references | Keep the child inside the parent, or check in code and tolerate dangling references on read |
| 9 | Showed you your own write immediately | Replicas lag; a read can return the value from before your write | A strongly consistent or quorum read where it matters |
| 10 | Serialised concurrent updates to one row with locks | Concurrent writes are reconciled afterwards — often last write wins, silently | Conditional writes on a version, or data types that merge |
| 11 | Refused a row that did not match the schema | Any shape is accepted; a year later three shapes coexist | A version field on every record, validation on write |
| 12 | Changed a column for every row in one statement | No statement rewrites every record | A resumable, rate-limited backfill, and readers that accept both shapes until it ends |
| 13 | Ran on one machine, where no single key could be hot | All traffic for one key lands on one partition, which has a fixed ceiling | A key with many values used about equally; split a hot key; cache the hottest reads |
| 14 | Let one customer’s rows grow without a limit | An item or a partition has a practical ceiling | Bound everything that grows: bucket by time, move long lists into their own items |
| 15 | Deleted a row, and reads did not slow down | A delete writes a tombstone that reads must skip until compaction removes it | Expire whole time buckets or tables instead of deleting rows one by one |
| 16 | Was the database everybody already knew how to secure, back up and upgrade | A second engine with its own defaults, backups, upgrades and licence | A named owner before the first write; read the defaults and the licence; rehearse a restore |
Four families, one mechanism, and the question each cannot answer
The name says what these databases are not, so it names nothing. Carlo Strozzi used it in 1998 for a relational database that simply had no SQL interface. The sense that stuck dates from 11 June 2009, when Johan Oskarsson organised a meetup in San Francisco about open-source distributed, non-relational databases and needed a short hashtag for it. Eric Evans suggested NoSQL. Later the industry back-formed it into "not only SQL". Behind the label sit four data models with almost nothing in common, and choosing the wrong family costs more than choosing the wrong product inside one.
| Family | What it stores | The question it answers | Examples | What it cannot answer |
|---|---|---|---|---|
| Key-value | A value behind a key; the store does not look inside it | “The session for this token”, in well under a millisecond | Redis, Valkey, Memcached; DynamoDB used this way | Anything about what is inside the values |
| Document | A nested record, with indexes on its fields | “This order, with everything in it” | MongoDB, Firestore, Couchbase, Cosmos DB | Relationships between documents; many-document changes cost extra |
| Wide-column | Rows grouped by a partition key and sorted inside it | “The last 50 messages in this channel”, at millions of writes a second | Cassandra, ScyllaDB, Bigtable, HBase | Anything that does not start from the partition key |
| Graph | Nodes and edges, both first-class, both with properties | “Friends of friends of friends”; the shortest path between two accounts | Neo4j, Amazon Neptune | Bulk scans and totals over everything; splitting across machines is hard |
Three neighbours are often counted in the same umbrella. Search engines such as Elasticsearch and OpenSearch answer full-text and faceted queries. Time-series stores answer "this metric over this window". Vector databases answer "what is most similar to this", and have their own page. All three are usually fed from a primary database rather than being one. The boundaries are also blurring from both sides. PostgreSQL has stored and indexed JSONJavaScript Object Notation — text format for dataA text format for passing data around: key–value pairs, lists, numbers and strings. It came out of JavaScript, but every language reads it now, and that is why most APIs answer in it rather than in anything more capable.All terms as jsonb since version 9.4 in 2014. MongoDB has had transactions since 2018, and Cassandra 6.0, in alpha in 2026, brings multi-partition ACIDAtomicity, Consistency, Isolation, Durability — the four promises of a transactionThe four guarantees of a database transaction: all of it happens or none of it does, it never leaves a declared rule broken, concurrent transactions do not see each other half-finished, and a confirmed commit survives a crash. Isolation comes with settings, and the default in most engines is weaker than the name suggests.Full article →All terms transactions. In April 2024 graphs got ISO/IEC 39075, the GQL query language — the first new ISO database language since SQL. So a family describes what a store does by default and cheaply, not the limit of what it can be made to do.
What the first three families share is the mechanism that lets them grow past one machine, and it is worth stating plainly because it is also the constraint. Every record is assigned to a node by hashing a key you chose when you designed the table. A request that carries that key goes to exactly one node and stays fast however much data exists in total. A request that does not carry it has to ask every node and merge the answers — or, in several engines, is simply refused. DynamoDB will scan a whole table if asked, and bills every item it reads. Cassandra refuses such a query unless you add ALLOW FILTERING, a name chosen to make you hesitate.
The same key decides a second thing: whether the load is spread at all. Hashing distributes keys evenly only when the keys themselves are varied and used about equally. Partition by country and the node holding your largest market takes most of the traffic while the others idle. Partition by day and every write in the system lands on one node until midnight. A hot partition does not announce itself as a hot partition. It looks like the whole cluster being slow, and adding nodes does not help, because the work was never divided. That is row 13, and the next section is about avoiding it.
The partition key — the one decision you cannot take back
A key in these stores usually has two parts, and they do different jobs. The partition key decides which node holds the record. The sort key — Cassandra calls it the clustering key — decides the order of records inside that partition. DynamoDB calls the pair a composite primary keythe column that identifies a rowThe column, or set of columns, whose value identifies exactly one row in a table and never repeats. A made-up number or a UUID usually makes a better primary key than a business value such as an email address, because business values change and every reference has to change with them.Full article →All terms. Cassandra writes it as PRIMARY KEY ((channel_id, bucket), message_id), where the inner brackets hold the partition key. Bigtable and HBase keep rows sorted by the whole key instead of hashing it, which makes range scans cheap and puts every new row at the end when keys only grow. MongoDB lets you choose either: a hashed shard key or a ranged one.
| A good partition key… | Why | The key that fails it |
|---|---|---|
| is carried by the important queries | A query without it touches every node (row 1) | An order id, when every screen starts from the customer |
| has many distinct values | Few values means few partitions, however many nodes you pay for | Country, status, plan type |
| is used about equally across its values | One busy value puts its whole load on one partition (row 13) | Today’s date; a celebrity’s account; one giant tenant |
| keeps each partition bounded in size | A partition that grows forever slows compaction, repair and reads (row 14) | A channel id alone; a sensor id with every reading since 2019 |
| does not only grow, in a store that keeps keys sorted | Every new write lands on the last range | A timestamp first in a Bigtable or HBase row key |
Discord’s message table shows all five rules at once. In 2017 the team moved from MongoDB to Cassandra. Their access pattern was almost always the same: read the newest messages in one channel. So the channel had to be in the partition key. But a channel alone would have been a partition that grows forever, and a busy channel collects years of messages. They added a time bucket to the partition key, about ten days of messages per bucket, which kept partitions under the 100 MB they had set as their own limit — far below the 2 GB Cassandra then allowed. The sort key was the message id, a Snowflake id that sorts by creation time, so "the newest fifty" is one short read from the end of one partition.
Buckets have a price, and it is paid by the quiet channels. A channel with three messages a month may need to walk back through a dozen empty buckets to find fifty messages, so the reading code has to know when to stop. The bucket size is a trade between the busiest partition’s size and the quietest channel’s number of reads. Buckets also fix only row 14. They do not fix row 13: a channel with a million people still sends all of its reads to the one bucket that holds today. Discord’s 2023 post describes exactly that, and the fix was not a new key but a layer in front of the database — the cases section has it.
Every engine puts a ceiling on one partition, and DynamoDB publishes its own. Each partition delivers at most 3,000 read units and 1,000 write units per second. A read unit is one strongly consistent read of up to 4 KB, or two eventually consistent ones; a write unit is one write of up to 1 KB. Adaptive capacity moves unused capacity towards a hot partition, but not past that ceiling. When one key genuinely needs more — a vote counter during a broadcast, say — the documented pattern is write sharding. Append a suffix from 0 to 9 to the key, so the writes spread over ten partitions, and read all ten and add them up when you need the total.
Replicas, quorums, and the write that quietly disappears
These stores keep several copies of every record on different nodes, which is what lets them survive a machine dying. Every one of them has to answer two questions. How many copies must confirm a write before the client is told it succeeded? And how many must be asked before a read returns? Amazon’s Dynamo paper of 2007 described the trade with three numbers that are still used for it. N is the number of copies, W the number that must confirm a write, R the number a read asks. When R + W is greater than N, every read asks at least one copy that took the latest write.
The arithmetic has two catches. First, it holds only while the same N nodes are the ones being asked. Dynamo-style stores use a "sloppy" quorum: when a replica is down, a stand-in node accepts the write and hands it over later, so during a failure R + W > N stops guaranteeing what it promised. Second, a quorum says which value a read sees; it says nothing about two writes to the same record that both succeeded. That is the conflict problem, and it gets its own diagram below.
The famous framing — consistency, availability, partition tolerance, pick two — was Eric Brewer’s conjecture in 2000, proved by Seth Gilbert and Nancy Lynch in 2002. It is true and almost useless day to day, because networks rarely partition. Daniel Abadi’s PACELC (2012) adds the normal case: if there is a partition, choose availability or consistency; else, choose latency or consistency. That "else" is the trade you live with. Waiting for a majority of replicas is slower than asking the nearest one, and the difference is paid on every read, not only during an incident.
| Engine | By default | The stronger option | What it costs |
|---|---|---|---|
| DynamoDB | Eventually consistent reads | A strongly consistent read of the table or a local index; transactions of up to 100 items | Twice the read units; never available on a global index |
| Cassandra | Consistency level per request; many drivers default to one replica | QUORUM or LOCAL_QUORUM; lightweight transactions (IF NOT EXISTS) on Paxos; Accord transactions in 6.0 | Latency on every call; several round trips for a lightweight transaction |
| MongoDB | Write concern majority since 5.0; read concern local | Read concern majority or linearizable; causally consistent sessions; transactions | Latency; transactions have their own limits and retries |
| Redis / Valkey cluster | Asynchronous replication | WAIT for a number of replicas | Reduces, but does not remove, the writes a failover can lose |
What the user sees matters more than the level’s name, so name the guarantee you need in the user’s terms. Eventual consistency means a change can be missing from the next screen and then appear, and two people can briefly see different values. Read-your-writes means you always see your own changes, while other people’s may lag — that is row 9, and the usual fix is to read your own writes from where you made them. Monotonic reads means you never see an older value after a newer one, which is what breaks when successive requests hit different replicas. A quorum read reflects the latest acknowledged write, at the cost of latency and of failing when too few replicas answer.
A documented guarantee is a claim, and claims get tested. In May 2020 Jepsen analysed MongoDB 4.2.6 and found that "even at the strongest levels of read and write concern, it failed to preserve snapshot isolation": transactions saw some but not all of an earlier transaction’s effects, and retries wrote the same value twice. The defaults for transactions were weaker still — read concern local and a write acknowledged by one node. MongoDB has since made majority the default write concern, from 5.0 in 2021. The lesson is not about one vendor. Read the independent analysis of the engine you choose, and test your own failure cases, because the guarantee you configured is the one you get only if the defaults and the documentation agree.
Two writes, one survivor
Row 10 is the quiet one. When two replicas accept a change to the same record and later have to reconcile, the common default is to keep the version with the later timestamp and discard the other. Cassandra does this for every column value; DynamoDB global tables do it between regions. That is not a merge but a silent loss, and it takes no error path in your application. It also depends on clocks agreeing between machines, which they never do exactly. In 2013 Jepsen ran Riak with last-write-wins through a network partition and lost 91% of the writes it had acknowledged. With strict quorums for every read and write the loss was 92%, because a quorum decides which value is read, not whether two writes merge. With a data type built to merge, it lost none.
So decide, per record type, what should happen when two writes collide. There are three real answers. A conditional write makes the second writer fail loudly: every engine here can write "only if the version is still 4" — a condition expression in DynamoDB, IF version = 4 in Cassandra, a filter on the version in MongoDB — and the loser reads again and retries. A merging data type keeps both: counters that add, sets that union, the family known as CRDTs, short for conflict-free replicated data types. And an append keeps both by never overwriting: store each change as its own item, and compute the current state from the list. Leaving it to the default means choosing to lose the edit you did not think about.
Model for the query, not for the entity
Relational modelling stores each fact once and assembles answers with joins at read time. Without joins (row 2), that assembly has to happen at design time instead. You store the answer in the shape the screen needs, and you accept storing some facts more than once. An order document that carries the customer’s name and address beside the line items is not a modelling failure — it is the model. The cost is that when the customer changes their name, several documents have to change, and your code is the only thing that knows which ones.
The method is a list before a schema. Write every access pattern the product needs as a sentence, then give each one a key. Here is a small chat product, in the style DynamoDB’s own guidance calls single-table design, where several kinds of item share one table and are told apart by key prefixes:
| Access pattern | Partition key | Sort key | Note |
|---|---|---|---|
| The newest 50 messages in a channel | CH#123#2026-10-01 | MSG#<id>, newest first | A ten-day bucket in the key (row 14) |
| Edit one message | CH#123#2026-10-01 | MSG#<id> | The client must know the channel and the bucket — so the id encodes the time |
| The channels a user belongs to | USER#42 | CH#<id> | One membership item per pair |
| The members of a channel | CH#123 (in an index) | USER#<id> | The same items, read through a global index with the keys swapped — and a lag (row 3) |
| Unread count per user and channel | USER#42 | UNREAD#CH#<id> | A counter kept on write, not a count on read (row 4) |
| Search messages by text | — | — | Not this store: a search index fed from the change stream |
Read the table for what it does not contain. There is no "all messages by this user" pattern, so no key serves it, and adding it later means writing every message a second time under a user key. The membership item exists twice in effect — once under the user, once through the index under the channel — and the index is updated asynchronously. And the last pattern is honestly sent elsewhere. A team that writes this list for real usually finds one or two patterns that do not fit any key, and that is the most useful finding of the exercise.
| Embed the related data when… | Reference it when… |
|---|---|
| it is read with the parent almost every time | it is often read on its own |
| the number of children is small and bounded — addresses, line items | the list can grow without a limit — comments, followers, events (row 14) |
| it belongs to one parent only | many parents share it — a product, a user’s profile |
| it must change atomically with the parent (row 6) | it changes on its own schedule, by other writers |
The second column’s middle row is where documents break. MongoDB caps one document at 16 MB and DynamoDB caps one item at 400 KB; MongoDB’s own documentation names "unbounded arrays" as an anti-pattern. A post that embeds its comments works until one post goes viral. Social feeds meet the same choice at scale. Twitter described writing each tweet into every follower’s cached timeline — fan-out on write — except for accounts with very many followers, whose tweets are merged in when the timeline is read. Every denormalised design ends up with a rule like that: duplicate where reads dominate, and stop duplicating where one write would turn into millions.
Secondary indexes are where the marketing and the mechanics part company. An index local to a partition is cheap and consistent, because it lives beside the data it indexes. An index that spans partitions — DynamoDB’s global secondary index, Cassandra’s materialised view — has to be maintained across nodes. So it is updated asynchronously, a read of it can miss a row you just wrote, and every write to the table becomes a write to each index as well. It is a real tool for secondary access paths and a poor substitute for a key that fits the primary one. It is also partitions of its own, which the 2015 DynamoDB case shows from the operator’s side.
This is the right shape
- The access patterns are known and narrow — fetch this thing by this id, over and over.
- Write volume is measured to be beyond one machine, and the key spreads it evenly.
- Records are self-contained: what is read together can be stored together.
- A short window of staleness is acceptable to the people using the product.
This is a relational problem
- New questions arrive that nobody anticipated, and each needs a different access path.
- Several records must change together or not at all — money, stock, bookings.
- The same fact is needed from many angles, so each copy multiplies the ways it can drift.
- The volume is large by intuition rather than by measurement — which is most of the time.
Running one — schema drift, deletes, capacity, defaults and the licence
"Schemaless" is the word that costs teams the most (row 11). There is always a schema; the only question is whether it is written down and enforced, or implied by whichever code last touched the collection. After a year, a document store holds three generations of record shape: some with a field, some without, some with the field holding a different type. Every read path then has to handle all three, and nothing tells you when one was missed. The teams that stay sane put a version number on every record, validate on write — MongoDB has enforced a $jsonSchema validator since 3.6, or the application can — and treat a change of shape as a migration with a plan.
Migrations themselves change character (row 12). No statement rewrites a field across the whole collection, so a change of shape becomes a background job that walks every record. It has to be resumable, rate-limited so it does not starve live traffic, and safe to run twice. Meanwhile the application reads both the old and the new shape. So the migration is not finished when the job finishes. It is finished when the code that understands the old shape is deleted — and that is the step nobody schedules. A lazy migration, which upgrades a record when it is next read, avoids the job but makes the old shape permanent for every record nobody reads.
Deletes are the surprise of row 15. In Cassandra and its relatives a delete does not remove data. It writes a tombstone, a marker saying "this was deleted at this time", which must outlive every replica that might still hold the old value. The default grace period, gc_grace_seconds, is ten days. Until compaction removes the tombstones, every read of that partition has to step over them: Cassandra warns after a thousand in one read and fails the read at a hundred thousand. A queue built on a table — insert, read, delete — therefore gets slower the more work it finishes. The fix is to delete in bulk shapes the engine handles cheaply: expire whole time buckets, or let a TTL, a time to live on each record, retire data that has a known lifespan. DynamoDB’s TTL removes expired items "typically within a few days" and without charge, so reads must still filter out what has expired.
Capacity and cost follow the key, too. On a pay-per-request service a scan bills every item it reads, a global index bills every write a second time, and a hot partition throttles while the table as a whole is mostly idle. That makes the bill a useful design review: a line item that grows faster than traffic usually points at a scan, an index nobody reads, or a key that should have been different.
Row 16 is about everything that is not the data model. Defaults are part of the product. MongoDB before 3.6 listened on every network interface, and authentication was off unless someone turned it on; 3.6, in November 2017, made it bind to localhost by default. The ransom wave in the cases section happened in the months before that change. Check what the engine listens on and whether it asks for a password before the first record goes in, not after. The licence is the other default, because several of these engines have changed theirs:
| Engine | What changed | The open alternative |
|---|---|---|
| MongoDB | AGPL → SSPL, October 2018; never approved as open source by the OSI | Compatible layers over PostgreSQL, such as FerretDB |
| Redis | BSD → RSALv2 or SSPLv1, March 2024; AGPLv3 added as a third option with Redis 8, May 2025 | Valkey, a Linux Foundation fork under BSD; 9.1 in May 2026 |
| Elasticsearch | Apache 2.0 → SSPL or Elastic License, 2021; AGPL option added, August 2024 | OpenSearch, Apache 2.0 |
| Cassandra | Unchanged: Apache 2.0, governed by the Apache Software Foundation | — |
Finally, the way back, because it exists. In 2018 the Guardian moved the database behind its CMS — about 2.3 million articles, some created two decades earlier — from MongoDB to PostgreSQL on Amazon RDS, storing the documents in jsonb columns. The reason was not the data model. Running the cluster had cost them "at least two months of engineering time a year", and two outages had stopped publishing for an hour or more each. A proxy sent every request to both databases and logged every difference in the answers until there were none, and ten months after the work began the old cluster was switched off. Modern relational engines do most of what a document store was adopted for. Reach for a second engine when a measured requirement demands it — write volume past one machine, a graph traversal, a cache with a millisecond budget — and be able to say which one. "We might need to scale" is not one of them.
Five cases, and what each one was missing
Each of these is one or two rows of the table at the top, met where it could no longer be ignored. Two are from the people who built the systems, one from an independent tester, one from the operator, and one from the attackers’ side.
| Case | What happened | What it shows |
|---|---|---|
| Amazon Dynamo, 2007 | The shopping cart merged divergent versions, so no added item was ever lost — and deleted items could come back | The merge rule is a product decision (row 10) |
| Riak under Jepsen, 2013 | Last-write-wins lost 91% of acknowledged writes through a partition; strict quorums, 92%; a merging set, none | A quorum does not fix overwrites; a merge does (rows 9–10) |
| DynamoDB, 2015 | Storage servers asked for their partition lists at once; lists grown by global indexes overloaded the metadata service; about 55% errors for hours | An index is partitions too (row 3) |
| MongoDB ransom wave, 2017 | At least 29,000 databases open to the internet without a password were wiped and held for ransom; payers got nothing back | Defaults are part of the product (row 16) |
| Discord, 2017–2023 | Time buckets in the key, a race that left half-written rows, tombstones that made a channel take 20 seconds to load, hot partitions under quorum reads | Rows 10, 13, 14 and 15 in one system — and a key that survived two engine changes |
The DynamoDB case is the one to show anyone who treats a global index as free. Early on Sunday 20 September 2015, a brief network disruption in US-East made many storage servers ask the metadata service for their "membership" — the list of partitions each one holds — at the same moment. Those lists had grown sharply in the preceding months, because customers had adopted global secondary indexes quickly, and AWS’s own summary explains why that mattered: global indexes "have their own set of partitions". The metadata service slowed under the large requests and began to time out. Servers that could not confirm their membership took themselves out of service and retried, which added load. From 2:37 a.m. Pacific time the error rate sat around 55%; AWS paused the requests at 5:06 to let the service recover, and most traffic was back by 7:10. The fixes were capacity, monitoring of membership size, longer timeouts and a metadata service split into segments. For a customer, the point is simpler: every index is more partitions somebody has to keep track of.
The ransom wave needed no vulnerability at all. In the last days of 2016 and the first of 2017, attackers searched the internet for MongoDB servers that answered without a password, copied or simply deleted the data, and left a note asking for bitcoin. By 10 January 2017, researchers tracking the attacks counted at least 29,000 databases wiped. Several groups were working the same servers and overwrote each other’s notes, so a victim could not tell who, if anyone, held a copy, and none of the victims who paid reported getting their data back. Every one of those servers was running as configured. The control that was missing was the one nobody thinks of as part of the data model: checking what the database listens on and whether it asks for credentials, before it holds anything worth stealing.
Where it degrades
A NoSQL store rarely fails because the engine was wrong for the job. It fails because the job was never written down. Almost every incident is one of these:
- A family chosen for a story about scale rather than for the shape of the data and the queries.
- A partition key chosen before the access patterns were listed, which the first new screen then invalidates (row 1).
- Scatter-gather queries added one at a time, until every read touches the whole cluster.
- A key with few values — country, day, status — that concentrates traffic on one partition (row 13).
- A list embedded in a record that nobody bounded, until one record hits the size limit (row 14).
- Last-write-wins left in place in a product where two clients genuinely edit the same record (row 10).
- Joins rebuilt in application code — the same work, without the planner or the index (row 2).
- No version field and no validation, so three record shapes coexist unnoticed (row 11).
- A queue or a log built on deletes, which slows down as tombstones pile up (row 15).
- A second database adopted for one feature and operated by nobody in particular thereafter (row 16).
The honest limit is that a partitioned store is fast at exactly the questions it was designed for and expensive at every other one. That is not a flaw to be fixed by a better product; it is the trade that buys the scale. A team that knows its questions gets predictable latency at any size. A team that does not will spend that advantage on scans, duplicate copies and a second database for the queries the first cannot answer. And none of the engines can decide what should happen when two people change the same thing at once. That is a product rule, and only the people who own the product can write it.
The practical check takes an afternoon. Write down every access pattern the product has today and the key that serves each; any pattern served by a scan is a finding. Look up the largest partition and the largest item, and the busiest key in the last week. Find every place two clients can write the same record and say what happens when they do. Check what the database listens on, whether it asks for a password, and when a restore was last rehearsed. And name the person who owns it.
When to use it
Use it when
- A known, narrow set of access patterns at a volume measured to exceed one machine — events, telemetry, messages.
- Records read and written whole that depend on nothing else — sessions, carts, device state.
- Caches, counters and rate limits, where a key-value store is simpler and faster than any alternative.
- Relationship traversal of unknown depth, which is what a graph engine exists to answer.
Avoid it when
- Anything where several records must change together, unless the engine offers multi-item transactions you have tested.
- Reporting and ad-hoc analysis, where the questions are not known and each needs a different access path.
- A product still discovering its access patterns, since the key you must pick first is the thing still moving.
- Volume you have assumed rather than measured, which a relational database would have carried comfortably.
Sources and method
What this page stands on
- 17 sources
- 4 documented failures
- Checked October 13, 2026
The failures this page learns from
Riak, DynamoDB, the MongoDB ransom wave and Discord, in “Five cases”- ResearchKyle Kingsbury — Jepsen: Riak (opens in a new tab)aphyr.com · May 19, 2013
- PostmortemSummary of the Amazon DynamoDB Service Disruption and Related Impacts in the US-East Region (opens in a new tab)Amazon Web Services · Sep 2015
- ArticleBrian Krebs — Extortionists Wipe Thousands of Databases, Victims Who Pay Up Get Stiffed (opens in a new tab)Krebs on Security · Jan 10, 2017
- PostmortemStanislav Vishnevskiy — How Discord Stores Billions of Messages (opens in a new tab)Discord · Jan 13, 2017
- PostmortemBo Ingram — How Discord Stores Trillions of Messages (opens in a new tab)Discord · Mar 6, 2023
Research
Dynamo, Bigtable and what CAP does and does not say- ResearchGiuseppe DeCandia et al. — Dynamo: Amazon’s Highly Available Key-value Store (opens in a new tab)ACM SOSP 2007 · 2007
- ResearchFay Chang et al. — Bigtable: A Distributed Storage System for Structured Data (opens in a new tab)OSDI 2006 · Google Research · 2006
- ResearchSeth Gilbert, Nancy Lynch — Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services (opens in a new tab)ACM SIGACT News · 2002
- ResearchEric Brewer — CAP twelve years later: How the “rules” have changed (opens in a new tab)IEEE Computer · 2012
- ResearchDaniel Abadi — Consistency Tradeoffs in Modern Distributed Database System Design (opens in a new tab)IEEE Computer · 2012
- ResearchJepsen: MongoDB 4.2.6 (opens in a new tab)jepsen.io · May 15, 2020
Practices and where the terms come from
the partition key, each store’s limits, and two migrations- DocumentationBest practices for designing and using partition keys effectively in DynamoDB (opens in a new tab)AWS Documentation
- DocumentationMongoDB Database Manual (opens in a new tab)MongoDB Docs
- DocumentationApache Cassandra Documentation (opens in a new tab)Apache Software Foundation
- StandardISO/IEC 39075:2024 — Database languages — GQL (opens in a new tab)ISO · Apr 2024
- ArticleRaffi Krikorian — Timelines at Scale (opens in a new tab)InfoQ · QCon 2012 · 2012
- ArticleBye bye Mongo, Hello Postgres (opens in a new tab)The Guardian Engineering Blog · Nov 30, 2018
Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.
Found this useful?
Share it with someone who is working on the same problem.