+1 (740) 926-6856

Unified Compaction Strategy: Moving Tables Off STCS and LCS on Cassandra 5

Choosing a compaction strategy used to be a three-way commitment. Size-tiered (STCS) for write-heavy tables, and accept read amplification and large transient disk needs. Leveled (LCS) for read-latency-sensitive tables, and accept the write amplification. Time-windowed (TWCS) for TTL'd time-series. Pick wrong, and the fix was an ALTER TABLE that recompacted the whole dataset.

Cassandra 5 adds Unified Compaction Strategy (UCS), which replaces that categorical choice with a dial. Tiered and leveled become two ends of one continuous parameter, and the strategy shards the token range so SSTable sizes stay bounded as a table grows. It is the most consequential operational change in the 5.0 line for most clusters — more so, for the average team, than vector search.

This post covers what the parameters mean, how we migrate a live table, and the cases where we still leave the old strategy alone.

The parameter that matters: scaling_parameters

UCS's behaviour comes mostly from one setting, expressed per level:

  • T4tiered, fanout 4: wait for roughly 4 similar-sized SSTables, then merge them. Low write amplification, higher read amplification. This is STCS-like behaviour.
  • L10leveled, fanout 10: keep one run per level, merge eagerly. Low read amplification, higher write amplification. This is LCS-like behaviour.
  • N — the midpoint (two SSTables per level, tiered and leveled collapse to the same thing).

You can set a list and get different behaviour per level, which is the genuinely new capability:

ALTER TABLE ks.events WITH compaction = {
  'class': 'UnifiedCompactionStrategy',
  'scaling_parameters': 'T8, T4, N, L4',
  'target_sstable_size': '1GiB',
  'base_shard_count': 4
};

Read that left to right as "cheap merges for fresh data, progressively tidier as data ages". The newest level absorbs the write burst tiered-style; the oldest levels — where most bytes and most reads live — end up leveled and read-efficient. Under STCS or LCS you could not express that; you chose one posture for the whole table and lived with the other side's cost.

Two more settings you will touch:

  • target_sstable_size (default around 1 GiB): the size UCS aims for. Smaller files compact faster and spread the transient disk cost, but inflate file counts and per-read overhead. We rarely go below 512 MiB.
  • base_shard_count: how many token-range shards the lowest levels are split across. Sharding is why UCS does not produce the multi-hundred-gigabyte SSTables that make STCS majors so hazardous. More shards mean more parallelism and smaller units of work; too many on a small table means pointless fragmentation.

What UCS actually buys you

Three things, in the order we notice them on real clusters:

  1. Bounded compaction work units. STCS on a large table eventually merges enormous files, needing free space on the order of the data being merged and occupying a compaction thread for hours. UCS's sharding keeps individual compactions small and parallelizable. Nodes with tight disk headroom benefit most.
  2. One strategy to tune instead of a migration to justify. Adjusting scaling_parameters is a dial you can turn as the workload's read/write ratio drifts. Moving STCS to LCS was a decision meeting; moving T4 to N is a change window.
  3. Density-aware behaviour. UCS makes its decisions on shard density rather than raw file counts, so a table does not change personality just because it grew.

What it does not buy you: it is not a fix for compaction that is behind because the disks are saturated, and it will not rescue a table whose partitions are 800 MB. Compaction strategy is a throughput and layout question. Read latency caused by modeling stays a modeling problem.

Migrating a live table

ALTER TABLE with a new compaction class takes effect cluster-wide, immediately, on every node — and every node begins reorganizing that table's SSTables at once. On a busy cluster that is how you turn a tuning change into an incident. Our sequence:

1. Pick a candidate, not the biggest table. Start with a mid-sized table that has a clear complaint attached to it: SSTables-per-read p95 in the double digits, or STCS compactions that need more disk than you are comfortable with. Record the baseline first — nodetool tablehistograms, nodetool tablestats, read p99 from the client side, pending compactions, disk used per node. Without a baseline you will have opinions instead of results.

2. Test on one node via JMX. Compaction parameters can be overridden per node through the CompactionParametersJson attribute on the table's ColumnFamilyStore MBean. That override is node-local and does not survive a restart — which is exactly what you want for a trial. Convert one node, let it settle, and compare its SSTables-per-read and compaction throughput against its peers carrying the same traffic. This step costs an afternoon and has saved us several rollbacks.

3. Estimate the rewrite. Switching strategy rewrites effectively the whole table on every node. Budget disk headroom (we want a comfortable margin free before starting, and UCS's sharding makes the transient requirement much smaller than an STCS major), and budget time: at a throttled 32–64 MB/s per node, a 1 TB table is measured in hours, not minutes.

4. Apply, then throttle deliberately. After the ALTER, control the burst with nodetool setcompactionthroughput and concurrent compactor count rather than hoping. Raise throughput during off-peak, lower it before your daily peak. Watch pending compactions drain rather than grow, and watch read p99 — it can rise during the reorganization before it falls.

5. Verify against the baseline. After it settles: SSTables-per-read p95 down, read p99 down or flat, compaction throughput sustainable, disk usage stable. If the numbers are not better, change scaling_parameters — more leveled if reads are still expensive, more tiered if compaction cannot keep up — rather than reverting on instinct.

6. Roll forward one table at a time. Never convert a whole keyspace in one window. Each ALTER is a cluster-wide rewrite, and stacking them competes for the same I/O.

Where we still say no

TTL'd time-series tables stay on TWCS. This is the honest caveat that gets left out of the release-notes summaries. TWCS's value is that expiry becomes file deletion: a time window's SSTable ages out, every cell in it is expired, the file is dropped whole, and no tombstone is ever processed at read time. UCS in the 5.0 line is not time-window aware in that sense. It can drop fully expired SSTables, but it will also merge data across time boundaries, which mixes old and new TTLs back together and puts you back to purging tombstones through compaction. If your table is uniform-TTL time-series, TWCS remains the right answer and the migration is not worth attempting.

Tables that are fine stay where they are. A small STCS table with single-digit SSTables-per-read and no disk pressure has nothing to gain. Compaction migrations cost cluster I/O; spend it where a metric is complaining.

Mid-upgrade clusters wait. Do not change compaction strategy while nodes are on mixed versions or while upgradesstables is still running. Finish the upgrade, let the cluster be boring for a week, then tune.

The short version

UCS is a real improvement and a reasonable default for new write-heavy tables on Cassandra 5: bounded compaction units, per-level tuning, one strategy to reason about. It is not an upgrade you perform on a whole cluster in an evening, and it is not a replacement for TWCS on TTL'd time-series. Convert deliberately, one table at a time, with a baseline you wrote down first.

If you are on 5.0 and unsure which of your tables would actually benefit, that is a short piece of work — a cluster and data-model review sizes it in days, and our operations retainer covers running the conversions table by table with the throttles minded.