DevelopmentSenior

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 thisA partitioned store insteadWhat you do instead
1Answered a question nobody thought of when the table was designedA query without the partition key asks every node — or is refusedList the access patterns first; choose the key so the important ones carry it
2Joined tables at read timeNo joins across partitions; the application makes one more request per related itemStore the answer in the shape the screen reads; fetch the rest in one batch
3Found rows by any indexed column, and the index was never behindAn index spanning partitions is updated asynchronously and can miss a write you just madeGlobal indexes for paths that tolerate a lag; read by key where it matters
4Counted, summed and grouped: COUNT, SUM, GROUP BYAn aggregate scans everything, or does not existMaintain counters on write; send the rest to an analytical store
5Sorted and paginated by any columnOrder exists only inside one partition, by its sort keyPut the order you need into the sort key; paginate from the last key seen
6Changed several rows in one transactionAtomic per item; multi-item transactions, where they exist, cost more and have limitsKeep what changes together in one item; use the engine’s transaction for the rest, within its limits
7Kept any column unique — one account per emailUniqueness only on the primary keyA second item keyed by the unique value, written in the same transaction
8Refused a reference to something that does not existNothing checks referencesKeep the child inside the parent, or check in code and tolerate dangling references on read
9Showed you your own write immediatelyReplicas lag; a read can return the value from before your writeA strongly consistent or quorum read where it matters
10Serialised concurrent updates to one row with locksConcurrent writes are reconciled afterwards — often last write wins, silentlyConditional writes on a version, or data types that merge
11Refused a row that did not match the schemaAny shape is accepted; a year later three shapes coexistA version field on every record, validation on write
12Changed a column for every row in one statementNo statement rewrites every recordA resumable, rate-limited backfill, and readers that accept both shapes until it ends
13Ran on one machine, where no single key could be hotAll traffic for one key lands on one partition, which has a fixed ceilingA key with many values used about equally; split a hot key; cache the hottest reads
14Let one customer’s rows grow without a limitAn item or a partition has a practical ceilingBound everything that grows: bucket by time, move long lists into their own items
15Deleted a row, and reads did not slow downA delete writes a tombstone that reads must skip until compaction removes itExpire whole time buckets or tables instead of deleting rows one by one
16Was the database everybody already knew how to secure, back up and upgradeA second engine with its own defaults, backups, upgrades and licenceA 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.

FamilyWhat it storesThe question it answersExamplesWhat it cannot answer
Key-valueA value behind a key; the store does not look inside it“The session for this token”, in well under a millisecondRedis, Valkey, Memcached; DynamoDB used this wayAnything about what is inside the values
DocumentA nested record, with indexes on its fields“This order, with everything in it”MongoDB, Firestore, Couchbase, Cosmos DBRelationships between documents; many-document changes cost extra
Wide-columnRows grouped by a partition key and sorted inside it“The last 50 messages in this channel”, at millions of writes a secondCassandra, ScyllaDB, Bigtable, HBaseAnything that does not start from the partition key
GraphNodes and edges, both first-class, both with properties“Friends of friends of friends”; the shortest path between two accountsNeo4j, Amazon NeptuneBulk 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 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 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.

In a distributed store every record is placed on a node by hashing a key you choose when you design the table. A request carrying that key goes to exactly one node and stays fast however much data exists in total; a request without it has to ask every node and merge the answers, or is refused outright. The key also decides whether load is spread at all — a low-cardinality key such as country or day concentrates traffic on one node, which from the outside looks like the whole cluster being slow. Adding an index later is easy; changing the partition key means rewriting every record into a new placement.

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 . 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…WhyThe key that fails it
is carried by the important queriesA query without it touches every node (row 1)An order id, when every screen starts from the customer
has many distinct valuesFew values means few partitions, however many nodes you pay forCountry, status, plan type
is used about equally across its valuesOne 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 sizeA 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 sortedEvery new write lands on the last rangeA 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.

Discord’s message table, as described in its 2017 post. With the channel alone as the partition key, one partition would hold every message a channel ever had, growing past the 100 MB the team set as its own limit — and large partitions slow compaction, repair and reads. Adding a ten-day time bucket to the key splits the same messages into partitions of bounded size, and the most common query, the newest fifty messages, reads only the newest bucket. Buckets cost the quiet channels, which may walk back through empty buckets, and they do not fix a busy channel: all of its reads still go to the one bucket that holds today.

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.

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, the replicas that took the write and the replicas the read asks must share at least one, so the read sees the latest acknowledged value. With W = 1 and R = 1 out of three, the read can land on a copy the write has not reached yet. Two caveats: during failures, Dynamo-style stores accept writes on stand-in nodes — a sloppy quorum — so the overlap is no longer guaranteed; and a quorum says nothing about two writes to the same record that both succeeded. That is a conflict, decided by the merge rule.

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.

EngineBy defaultThe stronger optionWhat it costs
DynamoDBEventually consistent readsA strongly consistent read of the table or a local index; transactions of up to 100 itemsTwice the read units; never available on a global index
CassandraConsistency level per request; many drivers default to one replicaQUORUM or LOCAL_QUORUM; lightweight transactions (IF NOT EXISTS) on Paxos; Accord transactions in 6.0Latency on every call; several round trips for a lightweight transaction
MongoDBWrite concern majority since 5.0; read concern localRead concern majority or linearizable; causally consistent sessions; transactionsLatency; transactions have their own limits and retries
Redis / Valkey clusterAsynchronous replicationWAIT for a number of replicasReduces, 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.

Two clients change the same cart at almost the same moment, on two replicas. Each write succeeds and is acknowledged. When the replicas reconcile, the common default keeps the version with the later timestamp and silently discards the other — B was told its write succeeded and is never told otherwise. Timestamps come from each machine’s own clock, so the later stamp is not even guaranteed to belong to the later write. The three alternatives keep both edits: a conditional write makes the second writer fail loudly and retry; a data type that merges, such as a CRDT set, keeps both items; and storing each change as its own appended item means nothing is ever overwritten.

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 patternPartition keySort keyNote
The newest 50 messages in a channelCH#123#2026-10-01MSG#<id>, newest firstA ten-day bucket in the key (row 14)
Edit one messageCH#123#2026-10-01MSG#<id>The client must know the channel and the bucket — so the id encodes the time
The channels a user belongs toUSER#42CH#<id>One membership item per pair
The members of a channelCH#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 channelUSER#42UNREAD#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 timeit is often read on its own
the number of children is small and bounded — addresses, line itemsthe list can grow without a limit — comments, followers, events (row 14)
it belongs to one parent onlymany 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:

EngineWhat changedThe open alternative
MongoDBAGPL → SSPL, October 2018; never approved as open source by the OSICompatible layers over PostgreSQL, such as FerretDB
RedisBSD → RSALv2 or SSPLv1, March 2024; AGPLv3 added as a third option with Redis 8, May 2025Valkey, a Linux Foundation fork under BSD; 9.1 in May 2026
ElasticsearchApache 2.0 → SSPL or Elastic License, 2021; AGPL option added, August 2024OpenSearch, Apache 2.0
CassandraUnchanged: 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.

CaseWhat happenedWhat it shows
Amazon Dynamo, 2007The shopping cart merged divergent versions, so no added item was ever lost — and deleted items could come backThe merge rule is a product decision (row 10)
Riak under Jepsen, 2013Last-write-wins lost 91% of acknowledged writes through a partition; strict quorums, 92%; a merging set, noneA quorum does not fix overwrites; a merge does (rows 9–10)
DynamoDB, 2015Storage servers asked for their partition lists at once; lists grown by global indexes overloaded the metadata service; about 55% errors for hoursAn index is partitions too (row 3)
MongoDB ransom wave, 2017At least 29,000 databases open to the internet without a password were wiped and held for ransom; payers got nothing backDefaults are part of the product (row 16)
Discord, 2017–2023Time 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 readsRows 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

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.