Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Cassandra Leveled Compaction Strategy (LCS)

Cassandra 5.0+

Starting with Cassandra 5.0, Unified Compaction Strategy (UCS) is the recommended compaction strategy for most workloads, including read-heavy patterns traditionally suited to LCS. UCS provides similar read amplification benefits with more adaptive behavior. LCS remains fully supported and is a proven choice for production deployments on earlier versions.

LCS organizes SSTables into levels where each level is 10x larger than the previous. Within each level (except L0), SSTables have non-overlapping token ranges, providing predictable read performance at the cost of higher write amplification.


Leveled Compaction Strategy was introduced to Cassandra in version 1.0 to address read amplification problems inherent in the original Size-Tiered Compaction Strategy. It follows the general LSM-tree leveled compaction approach used by systems such as LevelDB and RocksDB.

The core insight from LevelDB was that organizing SSTables into levels with non-overlapping key ranges within each level dramatically reduces the number of files that must be consulted during reads.

STCS groups SSTables by size and compacts similar-sized files together. While this minimizes write amplification, it creates a fundamental problem: any partition key might exist in any SSTable. A point query must potentially check every SSTable.

LCS inverts this trade-off. By ensuring that SSTables within each level (except L0) cover disjoint key ranges, a point query needs to consider at most one SSTable per non-empty L1+ level that could contain the key. The cost is higher write amplification, as data is rewritten each time it progresses through levels.

AspectSTCSLCS
SSTable organizationBy size similarityBy key range per level
Read amplificationHigh (check many SSTables)Low (one per level)
Write amplificationLowerHigher (compounds across levels)
Space amplificationMedium-HighLow
Compaction predictabilityVariableConsistent

LCS organizes SSTables into numbered levels (L0 through L8) with specific properties. The maximum level count is 9 (MAX_LEVEL_COUNT = 9 in the source code).

Level 0 (L0):

  • Receives memtable flushes directly
  • SSTables may have overlapping key ranges
  • Target size: 4 × sstable_size_in_mb (default: 640MB)
  • Stored in a HashSet (unordered by token range)
  • Conceptually acts as a buffer between memory and the leveled structure

Level 1+ (L1, L2, L3, ... L8):

  • SSTables have non-overlapping, contiguous key ranges
  • Stored in TreeSets sorted by first token (with SSTable ID as tiebreaker)
  • Each level has a target total size calculated as: fanout_size^level × sstable_size_in_mb
  • Individual SSTable size is fixed (default 160MB)

Level Size Targets (with defaults):

LevelTarget Size FormulaDefault Size
L04 × sstable_size640 MB
L1fanout × sstable_size1.6 GB
L2fanout² × sstable_size16 GB
L3fanout³ × sstable_size160 GB
L4fanout⁴ × sstable_size1.6 TB
L5fanout⁵ × sstable_size16 TB
L6fanout⁶ × sstable_size160 TB
L7fanout⁷ × sstable_size1.6 PB
L8fanout⁸ × sstable_size16 PB
Leveled Compaction Strategy (LCS)Leveled Compaction Strategy (LCS)Level 0 (L0) - many small, overlapping SSTablesLevel 1 (L1) - non-overlapping by key rangeLevel 2 (L2) - 10x size of L1, disjoint rangesSSTableSSTableSSTableSSTableSSTableSSTableSSTable[key A-M]SSTable[key N-T]SSTable[key U-Z]SSTable[key A-L]SSTable[key M-Z]LCS properties:Only L0 has overlapping SSTablesEach level has a target sizeHigher levels have larger, fewer fileswith disjoint key rangescompaction(merge overlapping)compaction(when L1 > target)
LCS Compaction Decision FlowLCS Compaction Decision FlowCheck level scores(highest level first)Any level score > 1.001?yesnoSelect first level (highest to lowest)where score > 1.001Level == L0?yesnoL0 count > 32?yesnoMay run STCS within L0(fallback mode)Select eligible L0 SSTables(by max timestamp ascending)Add overlapping L0 SSTablesAdd overlapping L1 SSTables(if size > sstable_size)Select next SSTable(round-robin from lastCompacted)Find overlapping L(n+1) SSTablesExecute compactionUpdate lastCompactedSSTablesTombstone compaction needed?yesnoFind SSTable withdroppable tombstones > thresholdRun single-SSTable compactionNo compaction needed

Compaction priority is determined by calculating a score for each level:

score=non-compacting bytes in levelmax bytes for level\text{score} = \frac{\text{non-compacting bytes in level}}{\text{max bytes for level}}

Compaction is triggered when score>1.001\text{score} > 1.001. The score is calculated using only the bytes from SSTables not currently involved in compaction. The compaction scheduler iterates from the highest level (L8) to the lowest, and selects the first level that exceeds the threshold — it does not compare scores across levels.

L0 compaction is triggered as part of the standard compaction selection process when no higher-priority levels require compaction. Unlike higher levels, L0 compaction is not driven directly by a score threshold. The candidate selection algorithm:

  1. Sorts eligible L0 SSTables by max timestamp ascending
  2. Iterates through sorted SSTables, adding each and its overlapping L0 peers into the candidate set
  3. Caps candidate count at max_threshold (default: 32)
  4. If the resulting L0 candidate set exceeds maxSSTableSizeInBytes, overlapping L1 SSTables are added and the compaction targets promotion out of L0; otherwise the compaction remains within L0
  5. Requires minimum 2 SSTables to proceed with compaction

All selected SSTables are merged. Output is written to L1 when compaction size exceeds maxSSTableSizeInBytes; otherwise the result remains in L0.

When L0 contains more than 32 SSTables (MAX_COMPACTING_L0 = 32) and STCS-in-L0 is not disabled, Cassandra may perform STCS-style compaction within L0, provided getSSTablesForSTCS(...) returns a non-empty bucket. This compacts similarly-sized L0 SSTables together, reducing SSTable count more quickly than waiting for L1 capacity. This behavior can be disabled with -Dcassandra.disable_stcs_in_l0=true.

When a level exceeds its size target (score > 1.001), the compaction process:

  1. Selects the next SSTable in round-robin order from lastCompactedSSTables[level] position
  2. Skips SSTables that are currently compacting or marked as suspect
  3. Identifies all overlapping SSTables in L(n+1)
  4. Merges and rewrites all selected SSTables to L(n+1)
  5. Updates lastCompactedSSTables[level] to track position for next round

This round-robin approach ensures fair distribution of compaction work across the token range.

The manifest tracks rounds without high-level compaction using NO_COMPACTION_LIMIT = 25. If 25 consecutive compaction rounds occur without selecting SSTables from higher levels, starved SSTables may be pulled into lower-level compactions to ensure data eventually progresses through levels.

When single_sstable_uplevel is enabled (default: false), and the compaction task contains exactly one SSTable and is not a tombstone compaction, Cassandra may create a SingleSSTableLCSTask that promotes the SSTable to a higher level without rewriting, subject to level placement rules enforced by the manifest.

Write amplification in LCS is determined by how many times data is rewritten as it moves through levels:

Write path for one piece of data:

  1. Written to memtable (memory)
  2. Flushed to L0 (1 write)
  3. Compacted L0 → L1 (1 write)
  4. Compacted L1 → L2 (potentially 10 writes, merging with ~10 L2 files)
  5. Compacted L2 → L3 (potentially 10 writes)
  6. Continue through higher levels...

The conceptual worst-case write amplification is approximately:

WAf×L\text{WA} \approx f \times L

Where ff = fanout (default 10) and LL = number of populated levels. This is a theoretical approximation, not a code-defined contract. Actual write amplification depends on workload characteristics, data distribution, overlap patterns, and whether optimizations like single_sstable_uplevel are enabled.

L1+ levels are maintained as non-overlapping by token range, so a point read needs to consider at most one SSTable per non-empty level that could contain the key. Bloom filters and range pruning further reduce actual I/O.

Structural upper bound from levels alone:

  • L0: overlap set (variable count, typically low under normal compaction)
  • L1–L8: at most one SSTable per non-empty level

Under healthy conditions with timely compaction, L0 typically contains only a few SSTables. The bounded nature of L1–L8 provides more predictable read latency than STCS, where a point query may need to check many SSTables.


The bounded number of SSTables per read provides consistent latency:

  • Bounded SSTable checks per level reduces read latency variance
  • Read performance is less sensitive to data age than with STCS

Unlike STCS, which may temporarily require 2× space during large compactions, LCS operates incrementally:

  • Compactions involve small, bounded sets of files
  • Temporary space overhead is minimal
  • Easier capacity planning

When no standard compaction candidate is found, LCS may attempt single-SSTable tombstone compaction. It scans levels from highest to lowest, looking for SSTables with droppable tombstone ratio exceeding tombstone_threshold, subject to safety checks via worthDroppingTombstones(...).

  • Data moves through levels, giving tombstones opportunities to be purged
  • Dedicated tombstone compaction path exists as a fallback

Compaction work is distributed evenly over time:

  • No massive compaction events
  • More predictable I/O patterns
  • Easier to provision for sustained throughput

The primary cost of LCS is rewriting data multiple times:

  • Each level transition involves merging with existing data
  • Actual write amplification depends on workload characteristics, data distribution, and configuration
  • SSD endurance is consumed faster

Write rate is bounded by how fast compaction can promote data:

  • If writes exceed L0→L1 compaction rate, L0 backs up
  • L0 backlog increases read amplification (defeating LCS purpose)
  • May require throttling writes

Write-heavy workloads may experience:

  • Compaction unable to keep pace
  • Growing pending compaction tasks
  • Disk I/O saturated by compaction

Time-series data has sequential writes and time-based queries:

  • LCS wastes effort organizing by key range
  • TWCS or UCS with tiered configuration may be more suitable
  • LCS key-range organization does not align with time-based access patterns

Large partitions may produce SSTables larger than sstable_size_in_mb, which can degrade compaction efficiency:

  • May stall compaction progress
  • Require data model changes to address

Workload PatternWhy LCS Works
Read-heavy workloadsLow read amplification pays for write cost
Point queriesBounded SSTable checks per query
Frequently updated rowsVersions consolidated quickly
Latency-sensitive readsPredictable, consistent response times
SSD storageHandles write amplification efficiently
Workload PatternWhy LCS Is Wrong
Write-heavy workloadsWrite amplification overwhelms I/O
Time-series dataTWCS is more efficient
Append-only logsSTCS or TWCS better suited
HDD storageRandom I/O from compaction is slow
Very large datasetsCompaction may not keep pace

CREATE TABLE my_table (
id uuid PRIMARY KEY,
data text
) WITH compaction = {
'class': 'LeveledCompactionStrategy',
-- Target size for each SSTable
-- Smaller = more SSTables, more compaction overhead
-- Larger = bigger compaction operations
'sstable_size_in_mb': 160, -- Default: 160MB
-- Size multiplier between levels (fanout)
-- Default 10 means L2 is 10x L1
'fanout_size': 10 -- Default: 10
};
ParameterDefaultDescription
sstable_size_in_mb160Target size for individual SSTables in megabytes. Smaller values increase SSTable count and compaction frequency; larger values reduce compaction overhead but increase individual compaction duration.
fanout_size10Size multiplier between adjacent levels. Level L(n+1) has a target capacity of fanout_size × L(n). Higher values reduce the number of levels but increase write amplification per level transition.
single_sstable_uplevelfalseWhen enabled, allows a single SSTable compaction task to be handled as a SingleSSTableLCSTask instead of a standard LeveledCompactionTask, provided the task is not a tombstone compaction and contains exactly one SSTable. Resulting level placement remains subject to manifest rules.

These options apply to all compaction strategies:

ParameterDefaultDescription
enabledtrueEnables background compaction. When set to false, automatic compaction is disabled but the strategy configuration is retained.
tombstone_threshold0.2Ratio of garbage-collectable tombstones to total columns that triggers single-SSTable compaction. A value of 0.2 means compaction is triggered when 20% of the SSTable consists of droppable tombstones.
tombstone_compaction_interval86400Minimum time in seconds between tombstone compaction attempts for the same SSTable. Prevents continuous recompaction of SSTables that cannot yet drop tombstones.
unchecked_tombstone_compactionfalseWhen true, bypasses pre-checking for tombstone compaction eligibility. Tombstones are still only dropped when safe to do so.
only_purge_repaired_tombstonesfalseWhen true, tombstones are only purged from SSTables that have been marked as repaired. Useful for preventing data resurrection in clusters with inconsistent repair schedules.
log_allfalseEnables detailed compaction logging to a separate log file in the log directory. Useful for debugging compaction behavior.

The following JVM option affects LCS behavior:

OptionDescription
-Dcassandra.disable_stcs_in_l0=trueDisables STCS-style compaction in L0. By default, when L0 accumulates more than 32 SSTables, Cassandra may perform STCS compaction within L0 if an eligible STCS bucket is found, to reduce the SSTable count more quickly. This option disables that behavior.

The target size for each level is calculated using the formula:

maxBytesForLevel(L)=fL×s\text{maxBytesForLevel}(L) = f^L \times s

Where:

  • LL = level number (1-8)
  • ff = fanout_size (default: 10)
  • ss = sstable_size_in_mb (default: 160 MB)

Special case for L0:

maxBytesForLevel(0)=4×s\text{maxBytesForLevel}(0) = 4 \times s

With default values (s=160MBs = 160\text{MB}, f=10f = 10):

LevelFormulaTarget Size
L04×160MB4 \times 160\text{MB}640 MB
L1101×160MB10^1 \times 160\text{MB}1.6 GB
L2102×160MB10^2 \times 160\text{MB}16 GB
L3103×160MB10^3 \times 160\text{MB}160 GB
L4104×160MB10^4 \times 160\text{MB}1.6 TB
L5105×160MB10^5 \times 160\text{MB}16 TB
L6106×160MB10^6 \times 160\text{MB}160 TB
L7107×160MB10^7 \times 160\text{MB}1.6 PB
L8108×160MB10^8 \times 160\text{MB}16 PB

The maximum dataset size per table with default settings is theoretically 16+ PB, though practical limits are reached well before this due to compaction throughput constraints.


LCS has high write amplification due to the promotion process. The following figures are theoretical maximums; actual amplification depends on workload characteristics, data distribution, update patterns, and configuration.

Per-level amplification (theoretical):

  • L0 → L1: SSTable overlaps with potentially all L1 SSTables → up to 10×\sim 10\times
  • L1 → L2: Same process with L2 overlaps → up to 10×\sim 10\times
  • Each subsequent level: up to f×\sim f\times where ff = fanout

Total write amplification (theoretical maximum):

Wtotalf×LW_{\text{total}} \approx f \times L

Where:

  • ff = fanout (default: 10)
  • LL = number of levels data traverses

Example: 100GB dataset with 5 levels (worst case):

W=10×5=50×W = 10 \times 5 = 50\times

In practice, write amplification is often lower due to factors like non-uniform key distribution, single_sstable_uplevel optimization, and varying overlap patterns. However, the high theoretical amplification makes LCS generally unsuitable for write-heavy workloads.


LCS provides predictable, low read amplification through its non-overlapping level structure:

For a single partition read:

  1. Check L0 SSTables (overlapping, count varies with write rate)
  2. Check at most 1 SSTable per level L1-L8 (non-overlapping)

Structural bound from leveled layout:

  • L0: all overlapping SSTables that must be considered
  • L1–L8: at most one SSTable per non-empty level that could contain the key

The key advantage is predictability: LCS bounds the number of SSTables that must be consulted per read, while STCS SSTable count can grow unbounded.


Symptoms:

  • L0 SSTable count growing (warning at 5+, critical at 32+)
  • Read latency increasing proportionally to L0 count
  • Compaction pending tasks growing
  • At 32+ L0 SSTables, STCS fallback may be triggered

Diagnosis:

Terminal window
nodetool tablestats keyspace.table | grep "SSTables in each level"
# Output: [15, 10, 100, 1000, ...]
# 15 L0 SSTables indicates moderate backlog
# Check if L0 size exceeds target (4 × sstable_size = 640MB default)
nodetool tablestats keyspace.table | grep -E "Space used|SSTable"

Causes:

  • Write rate exceeds L0→L1 compaction throughput
  • Insufficient compaction threads
  • Disk I/O bottleneck
  • Large L1 causing extensive overlap during L0→L1 compaction

Solutions:

  1. Increase compaction throughput:

    Terminal window
    nodetool setcompactionthroughput 128 # MB/s
  2. Add concurrent compactors:

    Terminal window
    nodetool setconcurrentcompactors 4
  3. Reduce write rate temporarily

  4. Consider switching to STCS if write-heavy

  5. If L0 exceeds 32 SSTables, STCS fallback may be triggered (can be disabled with -Dcassandra.disable_stcs_in_l0=true if necessary)

Issue 2: Large Partitions Stalling Compaction

Section titled “Issue 2: Large Partitions Stalling Compaction”

Symptoms:

  • Compaction stuck at same percentage
  • One SSTable significantly larger than sstable_size_in_mb

Diagnosis:

Terminal window
nodetool tablestats keyspace.table | grep "Compacted partition maximum"
# Output: Compacted partition maximum bytes: 2147483648
# 2GB partition exceeds 160MB target

Cause:

When a single partition exceeds sstable_size_in_mb, the resulting SSTable is "oversized" and may not compact efficiently.

Solutions:

  1. Fix data model to break up large partitions:

    -- Add time bucket to partition key
    PRIMARY KEY ((user_id, date_bucket), event_time)
  2. Increase SSTable size (affects all compaction):

    ALTER TABLE keyspace.table WITH compaction = {
    'class': 'LeveledCompactionStrategy',
    'sstable_size_in_mb': 320
    };

Issue 3: Write Amplification Overwhelming Disks

Section titled “Issue 3: Write Amplification Overwhelming Disks”

Symptoms:

  • Disk throughput at 100%
  • High iowait in system metrics
  • Write latency increasing

Diagnosis:

Terminal window
iostat -x 1
# Check %util approaching 100%
nodetool compactionstats
# Check bytes compacted vs. bytes written

Solutions:

  1. Switch to STCS for write-heavy tables:

    ALTER TABLE keyspace.table WITH compaction = {
    'class': 'SizeTieredCompactionStrategy'
    };
  2. Throttle compaction to reduce I/O competition:

    Terminal window
    nodetool setcompactionthroughput 32
  3. Add more nodes to spread write load


ALTER TABLE keyspace.table WITH compaction = {
'class': 'LeveledCompactionStrategy',
'sstable_size_in_mb': 160 -- Default, good for most cases
};
ALTER TABLE keyspace.table WITH compaction = {
'class': 'LeveledCompactionStrategy',
'sstable_size_in_mb': 320 -- Accommodate larger partitions
};
ALTER TABLE keyspace.table WITH compaction = {
'class': 'LeveledCompactionStrategy',
'sstable_size_in_mb': 256,
'fanout_size': 10
};

MetricHealthyWarningCritical
L0 SSTable count≤45-15>32 (STCS fallback may be triggered)
Pending compactions<2020-50>50
Level distributionPyramid shapeL0 growingL0 >> L1
Write latencyStableIncreasingSpiking
Level score (any level)<1.01.0-1.5>1.5
Terminal window
# SSTable count per level
nodetool tablestats keyspace.table | grep "SSTables in each level"
# Expected output for healthy LCS:
# SSTables in each level: [2, 10, 100, 500, 0, 0, 0, 0, 0]
# L0 L1 L2 L3 L4 L5 L6 L7 L8
# Warning (L0 building up):
# SSTables in each level: [12, 10, 100, 500, 0, 0, 0, 0, 0]
# Critical (L0 backlog, STCS will kick in):
# SSTables in each level: [45, 10, 100, 500, 0, 0, 0, 0, 0]
# Check compaction pending tasks
nodetool compactionstats
# View detailed level information
nodetool cfstats keyspace.table
# Per-level SSTable counts
org.apache.cassandra.metrics:type=Table,keyspace=*,scope=*,name=SSTablesPerLevel
# Compaction bytes written
org.apache.cassandra.metrics:type=Table,keyspace=*,scope=*,name=BytesCompacted
# Pending compaction bytes estimate
org.apache.cassandra.metrics:type=Table,keyspace=*,scope=*,name=PendingCompactions
# Compaction throughput
org.apache.cassandra.metrics:type=Compaction,name=BytesCompacted

A healthy LCS table should show:

  1. L0: Small count (typically 0-4 SSTables)
  2. L1: Multiple SSTables up to its configured capacity; exact count depends on SSTable sizes and compaction progress
  3. Higher levels: Level capacity increases geometrically with depth; SSTable distribution often trends that way under steady-state compaction

This section documents implementation details from the Cassandra source code that affect operational behavior.

When an SSTable is added to the manifest (e.g., after streaming or compaction), the level assignment follows these rules:

  1. Recorded level check: Each SSTable stores its intended level in metadata via getSSTableLevel()
  2. Overlap verification for L1+: Before placing an SSTable in L1 or higher, the manifest checks for overlaps with existing SSTables in that level
  3. Demotion to L0: If overlap is detected (before.last >= newsstable.first or after.first <= newsstable.last), the SSTable is demoted to L0 regardless of its recorded level

This behavior ensures the non-overlapping invariant is maintained even when SSTables arrive from external sources (streaming, sstableloader).

LeveledGenerations:
├── L0: HashSet<SSTableReader> // Unordered, overlapping allowed
└── levels[0-7]: TreeSet<SSTableReader> // L1-L8, sorted by first token
└── Comparator: firstKey, then SSTableId (tiebreaker)
LeveledManifest:
├── generations: LeveledGenerations
├── lastCompactedSSTables[]: SSTableReader // Round-robin tracking per level
├── compactionCounter: int // For starvation prevention
└── levelFanoutSize: int // Default: 10

The compaction task selects its writer based on the operation type. Based on compaction writer implementation (not shown in the LCS strategy source):

ConditionWriterBehavior
Major compactionMajorLeveledCompactionWriterFull reorganization, respects level structure
Standard compactionMaxSSTableSizeWriterOutputs SSTables at target size, assigned to destination level

During streaming operations that require anti-compaction (splitting SSTables by token range), LCS groups SSTables in batches of 2 per level to maintain level-specific guarantees while processing.

ConstantValueDescription
MAX_LEVEL_COUNT9L0 through L8
MAX_COMPACTING_L032Threshold for L0 STCS fallback
NO_COMPACTION_LIMIT25Rounds before starvation prevention
Minimum SSTable size1 MiBValidation constraint
Minimum fanout size1Validation constraint
Default fanout10Level size multiplier
Default SSTable size160 MiBTarget per-SSTable size