Apache Cassandra is an open-source, distributed database built for workloads that write a lot of data, must stay available when servers or whole data centres fail, and need to grow by adding machines. It has no primary node: every node accepts reads and writes. You query it with CQL, a language that looks like SQL but has no joins and expects you to design each table around one query.
Cassandra started at Facebook, was open-sourced in 2008 and has been a top-level Apache project since 2010, under the Apache License 2.0. The current major release line is 5.0.
When Cassandra fits, and when it does not
| Good fit | Poor fit |
|---|---|
| High, steady write volume: events, messages, sensor readings, activity history | Ad hoc queries, reporting, joins and aggregations across tables |
| Queries known in advance and always keyed by an ID | Queries that change often or filter on arbitrary columns |
| Multi-region deployment with local reads and writes | Multi-row, multi-table ACID transactions |
| Uptime that must survive node and data centre loss | Small datasets that fit on one relational server |
If you are deciding between Cassandra and a relational database for a few gigabytes of related business data, choose the relational database. Cassandra gives up joins, flexible querying and general transactions to get scale and availability. That trade pays off only when you need what it buys.
How a cluster works
The ring and partitioning
Every row belongs to a partition, identified by its partition key. Cassandra hashes the partition key to a token and assigns ranges of tokens to nodes, which are pictured as a ring. Any node can work out from the key alone which nodes own a row, so there is no central directory to consult or to fail.
Adding a node hands it a share of the token ranges, and the existing nodes stream the matching data to it. The application keeps running and its queries do not change.
Replication
Each partition is stored on several nodes. The replication factor (RF) is set per keyspace and, with NetworkTopologyStrategy, per data centre. RF 3 is the usual choice in production: three nodes hold a copy of every partition. Nodes learn about each other and detect failures through a gossip protocol.
Tunable consistency
For each query, the client chooses how many replicas must respond before the operation counts as successful:
| Consistency level | Replicas that must respond (RF 3) | Effect |
|---|---|---|
ONE | 1 | Fastest, most available; a read may return stale data |
QUORUM | 2 | A majority across the whole cluster |
LOCAL_QUORUM | 2 in the local data centre | Majority without waiting on other regions |
ALL | 3 | Strongest; fails if any replica is down |
The rule to remember: if read replicas plus write replicas is greater than RF, a read always overlaps with the latest successful write. With RF 3, writing and reading at QUORUM (2 + 2 > 3) gives consistent reads while tolerating one node down. Writing at ONE and reading at ONE is eventually consistent.
When two writes to the same column conflict, the one with the later timestamp wins. This is why clock synchronisation between nodes matters.
The write path
Cassandra's storage engine is a log-structured merge tree, which is what makes writes cheap:
- The write is appended to the commit log on disk for durability.
- It is applied to an in-memory memtable.
- When the memtable fills, it is flushed to disk as an immutable SSTable.
- Compaction merges SSTables in the background and discards overwritten and deleted data.
Nothing is updated in place. An UPDATE is a new write with a newer timestamp, and a DELETE writes a marker called a tombstone. A read may have to merge several SSTables, so reads generally cost more than writes.
The data model
The terms you will meet:
- A keyspace is the top-level container, like a database. It holds the replication settings.
- A table has a declared schema of typed columns. Older material calls tables "column families" and describes Cassandra as schema-free. That describes the Thrift API, which was removed in Cassandra 4.0. With CQL you define tables and columns up front, and can add columns later with
ALTER TABLE. - The primary key has two parts. The partition key decides which nodes store the row. The optional clustering columns decide the sort order of rows inside a partition.
Start cqlsh against a single-node development cluster and create a keyspace and a table:
CREATE KEYSPACE shop
WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': 1};
CREATE TABLE shop.orders_by_customer (
customer_id uuid,
order_time timestamp,
order_id uuid,
status text,
total decimal,
PRIMARY KEY ((customer_id), order_time, order_id)
) WITH CLUSTERING ORDER BY (order_time DESC, order_id ASC);
datacenter1 is the data centre name in a default installation; use nodetool status to see yours. A replication factor of 1 is for a one-node test cluster only.
All of one customer's orders live in one partition, sorted newest first:
INSERT INTO shop.orders_by_customer (customer_id, order_time, order_id, status, total)
VALUES (5b6962dd-3f90-4c93-8f61-eabfa4a803e2, '2026-10-01 09:15:00+0000',
0d1c4d6e-8a57-4f0b-9c55-3a4c2f1b7e10, 'shipped', 75.00);
INSERT INTO shop.orders_by_customer (customer_id, order_time, order_id, status, total)
VALUES (5b6962dd-3f90-4c93-8f61-eabfa4a803e2, '2026-10-05 17:40:00+0000',
7f3e2a90-1b6c-4d2e-a8f4-6c9d0e5b4a21, 'new', 45.00);
SELECT order_time, status, total
FROM shop.orders_by_customer
WHERE customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2
LIMIT 10;
order_time | status | total
---------------------------------+---------+-------
2026-10-05 17:40:00.000000+0000 | new | 45.00
2026-10-01 09:15:00.000000+0000 | shipped | 75.00
That query touches one partition on one set of replicas, however large the cluster. This is the access pattern Cassandra is built for.
Design tables from queries
Now ask a question the table was not designed for:
SELECT * FROM shop.orders_by_customer WHERE status = 'new';
InvalidRequest: Error from server: code=2200 [Invalid query] message="Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING"
Without the partition key Cassandra would have to scan every node. Adding ALLOW FILTERING makes the query run, and makes it slower as the cluster grows. The Cassandra answers are:
- A second table keyed for the second query, for example
orders_by_status, written by the application alongside the first. Duplicating data across tables is normal practice in Cassandra, not a design fault. - A secondary index. Storage-attached indexes (SAI), added in Cassandra 5.0, are the current option and are far more usable than the older index types. They work best when the query also restricts the partition key.
In relational design you model the entities and then write queries. In Cassandra you list the queries first and build a table for each.
Features in current versions
- Lightweight transactions give compare-and-set on a single partition with
IF NOT EXISTSorIF column = value. They use a consensus round and are much slower than plain writes. Use them for things like unique usernames, not for every write. - Time to live (TTL) expires data automatically:
INSERT ... USING TTL 86400. - Cassandra 5.0 added storage-attached indexes, a
vectordata type with approximate nearest-neighbour search, the unified compaction strategy and support for running on Java 17. - Windows is not supported. Since Cassandra 4.0 it runs only on Linux and other Unix-like systems; on a Windows machine you use WSL2 or Docker.
Managed services expose the same CQL API if you do not want to run nodes yourself, including DataStax Astra DB and Amazon Keyspaces. ScyllaDB is a separate, CQL-compatible database.
Common mistakes
- Relational modelling. Normalised tables that would need joins do not work. Model one table per query.
- Unbounded partitions. A partition that grows forever, such as all readings for a sensor, becomes slow to read and repair. Add a time bucket to the partition key, for example
(sensor_id, day). - Queue-style workloads. Inserting and deleting rows in the same partition builds up tombstones, which reads must skip. Past a threshold the query aborts.
ALLOW FILTERINGin application code. Fine for a one-off check on a test cluster, a latent outage in production.- Skipping repair. Replicas drift apart after failures. Run
nodetool repairregularly, and at least once within each table'sgc_grace_seconds(ten days by default), or deleted data can reappear. SimpleStrategyin production. It ignores racks and data centres. UseNetworkTopologyStrategyfrom the start.- A read-before-write habit. Reading a row to decide what to write doubles the cost and is not atomic. Design writes to be idempotent.
Further reading
Get the weekly commit
New database deep dives every week.
