← Back to list

Why CockroachDB Uses LSM Trees (And Why B-Trees Fail at Scale)

In this post we will walk through exactly how LSM trees work - from the in-memory MemTable that absorbs every write, to the immutable SST

Meenakshi Kumari · 2026-04-28 13:30 · 0 claps · 6.5 min read paywalled
#database-internals #cockroachdb #backend-engineering #distributed-system-design #lsm-tree
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Why CockroachDB Uses LSM Trees (And Why B-Trees Fail at Scale)

This is in continuation to my previous story CRDB Key-Value Storage

In this post we will walk through exactly how LSM trees work — from the in-memory MemTable that absorbs every write, to the immutable SST files on disk, to the background compaction process that keeps everything sorted. We will also cover bloom filters, tombstones, MVCC versioning, and the three fundamental trade-offs (write, read, and space amplification) that every engineer working with these databases should understand.

The Problem LSM Solves

Traditional B-Tree databases write directly to disk at random locations:

B-Tree write: “update row at page 4821”

Disk: [page1][page2].......[page4821]...[pageN]                                                                                                                             
                                ▲                                                                                                                                           
                                │ seek head here                                                                                                                            
                                │ write 8KB

Random I/O — disk head physically moves to location                                                                                                                     
HDDs: 5-10ms per seek × millions of writes = bottleneck                                                                                                                 
Even SSDs: random writes wear out cells faster

LSM’s insight: always write sequentially, never randomly. Sort it out later.

Level 0 — The Write Path (MemTable)

 Every write goes to memory first:

                      INCOMING WRITES
                           │                                                                                                                                                
      INSERT (1001,"INFY",50)   UPDATE (1001,"RELI",qty=80)
      INSERT (2005,"INFY",200)  DELETE (1001,"TCS")                                                                                                                         
                           │                                                                                                                                                
                           ▼                                                                                                                                                
  ┌────────────────────────────────────────────────────┐                                                                                                                    
  │                    MemTable                        │                                                                                                                    
  │              (in RAM — sorted by key)              │                                                                                                                    
  │                                                    │                                                                                                                    
  │  /Table/54/1/1001/"INFY"/0  @T3  → {qty:50}        │
  │  /Table/54/1/1001/"RELI"/0  @T4  → {qty:80}        │                                                                                                                     
  │  /Table/54/1/1001/"TCS"/0   @T5  → TOMBSTONE       │                                                                                                                     
  │  /Table/54/1/2005/"INFY"/0  @T3  → {qty:200}       │                                                                                                                     
  │                                                    │                                                                                                                    
  │  (Red-Black tree or skiplist internally)           │                                                                                                                    
  │  Size limit: ~64MB                                 │                                                                                                                    
  └────────────────────────────────────────────────────┘                                                                                                                    
                           │                                                                                                                                                                
                           │ also written to                                                                                                                                                
                           ▼                                                                                                                                                                
  ┌────────────────────────────────────────────────────┐                                                                                                                    
  │                  WAL (Write-Ahead Log)             │                                                                                                                    
  │           (sequential append to disk)              │                                                                                                                    
  │                                                    │                                                                                                                    
  │  [T3: INSERT 1001/INFY][T3: INSERT 2005/INFY]      │                                                                                                                     
  │  [T4: UPDATE 1001/RELI][T5: DELETE 1001/TCS]...    │                                                                                                                     
  │                                                    │                                                                                                                    
  │  Purpose: crash recovery only                      │                                                                                                                    
  │  If node dies → replay WAL → rebuild MemTable      │                                                                                                                    
  └────────────────────────────────────────────────────┘ 

Write is complete the moment it hits MemTable + WAL. No disk seek. Always sequential.

Tombstome: A tombstone is how LSM handles deletes. Because SST files are immutable (you cannot modify a file once written), you cannot actually remove a key from an existing SST file. So instead of removing it, you write a special marker that says “this key is deleted.

MemTable Fills Up → Flush to L0

When MemTable hits ~64MB, it freezes and flushes to disk as an SST file (Sorted String Table):

MemTable FULL → freeze → become “Immutable MemTable” New writes go to a fresh empty MemTable simultaneously.

Immutable MemTable flushes to disk in background.

                              FLUSH                                                                                                                                                 
                                │                                                                                                                                                   
                                ▼
  ┌─────────────────────────────────────────────────────────────┐                                                                                                           
  │                       LEVEL 0 (L0)                          │                                                                                                           
  │                    (on disk — SST files)                    │                                                                                                           
  │                                                             │                                                                                                           
  │  ┌──────────────────┐  ┌──────────────────┐                 │                                                                                                            
  │  │    SST file 1    │  │    SST file 2    │                 │                                                                                                            
  │  │  (oldest flush)  │  │  (newest flush)  │                 │                                                                                                            
  │  │                  │  │                  │                 │                                                                                                            
  │  │ 1001/"INFY" @T1  │  │ 1001/"INFY" @T3  │← NEWER version. │                                                                                                             
  │  │ 1001/"RELI" @T1  │  │ 1001/"RELI" @T4  │                 │                                                                                                             
  │  │ 2005/"INFY" @T1  │  │ 1001/"TCS"  @T5  │← TOMBSTONE      │                                                                                                             
  │  │ 2005/"WIPRO"@T1  │  │ 2005/"INFY" @T3  │                 │                                                                                                             
  │  └──────────────────┘  └──────────────────┘                 │                                                                                                            
  │                                                             │                                                                                                           
  │  KEY RANGES OVERLAP between files ← this is L0's property   │                                                                                                            
  │  Both files have 1001/"INFY" — different timestamps         │                                                                                                            
  └─────────────────────────────────────────────────────────────┘ 

L0 files can have overlapping key ranges — that’s okay because we know newer file = newer version.

The Full LSM Structure — All Levels

                          WRITES
                             │                                                                                                                                              
                             ▼
                ┌────────────────────────┐                                                                                                                                  
                │       MemTable         │ RAM ~64MB
                │   (mutable, sorted)    │                                                                                                                                  
                └────────────┬───────────┘                                                                                                                                  
                             │ flush when full                                                                                                                              
                             ▼                                                                                                                                              
  ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ DISK ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─                                                                                                                      

  ┌──────────────────────────────────────────────────────┐                                                                                                                  
  │  L0  │ SST1 │ SST2 │ SST3 │ SST4 │                   │  ~256MB                                                                                                           
  │      │ overlapping key ranges allowed                │                                                                                                                  
  └──────────────────────────────────────────────────────┘                                                                                                                  
                       │ compact when L0 has 4+ files                                                                                                                       
                       ▼                                                                                                                                                    
  ┌──────────────────────────────────────────────────────┐                                                                                                                  
  │  L1  │=====│=====│=====│=====│=====│=====│=====│     │  ~256MB                                                                                                           
  │      │ NO overlapping ranges — each file owns a span │                                                                                                                  
  └──────────────────────────────────────────────────────┘                                                                                                                  
                       │ compact when L1 exceeds size                                                                                                                       
                       ▼                                                                                                                                                    
  ┌──────────────────────────────────────────────────────┐                                                                                                                  
  │  L2  │==│==│==│==│==│==│==│==│==│==│==│==│==│==│     │  ~2.5GB                                                                                                            
  │      │ NO overlapping ranges                         │                                                                                                                  
  └──────────────────────────────────────────────────────┘                                                                                                                  
                       │                                                                                                                                                    
                       ▼                                                                                                                                                    
  ┌──────────────────────────────────────────────────────┐                                                                                                                  
  │  L3  │=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│     │  ~25GB                                                                                                              
  │      │ NO overlapping ranges                         │                                                                                                                  
  └──────────────────────────────────────────────────────┘                                                                                                                  
                       │                                                                                                                                                    
                       ▼                                                                                                                                                    
  ┌──────────────────────────────────────────────────────┐                                                                                                                  
  │  L4  │=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│     │  ~250GB                                                                                                             
  │      │ NO overlapping ranges                         │                                                                                                                  
  └──────────────────────────────────────────────────────┘                                                                                                                  
                       │                                                                                                                                                    
                       ▼                                                                                                                                                    
  ┌──────────────────────────────────────────────────────┐
  │  L5  │=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│=│     │  ~2.5TB                                                                                                             
  │      │NO overlapping ranges — bulk of data lives here│                                                                                                                 
  └──────────────────────────────────────────────────────┘      
  • Size multiplier per level: 10x.
  • Most data lives at the bottom. Top levels are tiny.

What Is Inside an SST(Sorted String Table) File

Each SST file is an immutable sorted file on disk.

SST File Layout:
  ┌─────────────────────────────────────────────────────┐                                                                                                                   
  │                   DATA BLOCKS                       │
  │  ┌─────────────────────────────────────────────┐    │                                                                                                                    
  │  │ Block 1 (4KB)                               │    │
  │  │  /Table/54/1/1001/"INFY"/0 @T3 → {qty:50}   │    │                                                                                                                     
  │  │  /Table/54/1/1001/"RELI"/0 @T4 → {qty:80}   │    │                                                                                                                     
  │  └─────────────────────────────────────────────┘    │                                                                                                                    
  │  ┌─────────────────────────────────────────────┐    │                                                                                                                    
  │  │ Block 2 (4KB)                               │    │                                                                                                                    
  │  │  /Table/54/1/1001/"TCS"/0  @T5 → TOMBSTONE  │    │                                                                                                                     
  │  │  /Table/54/1/2005/"INFY"/0 @T3 → {qty:200}  │    │                                                                                                                     
  │  └─────────────────────────────────────────────┘    │                                                                                                                    
  │                                                     │                                                                                                                   
  │                   INDEX BLOCK                       │                                                                                                                   
  │  ┌─────────────────────────────────────────────┐    │                                                                                                                    
  │  │  Block 1 → first key: 1001/"INFY", offset:0 │    │                                                                                                                    
  │  │  Block 2 → first key: 1001/"TCS",  offset:4K│    │                                                                                                                    
  │  └─────────────────────────────────────────────┘    │                                                                                                                    
  │                                                     │                                                                                                                   
  │                  BLOOM FILTER                       │                                                                                                                   
  │  ┌─────────────────────────────────────────────┐    │
  │  │  Probabilistic: "does key X exist here?"    │    │                                                                                                                    
  │  │  False positive possible, false negative NOT│    │                                                                                                                    
  │  │  Fits in RAM (~10 bits per key)             │    │                                                                                                                    
  │  └─────────────────────────────────────────────┘    │                                                                                                                    
  │                                                     │                                                                                                                   
  │                    FOOTER                           │                                                                                                                   
  │  ┌─────────────────────────────────────────────┐    │                                                                                                                    
  │  │  smallest_key, largest_key, checksum        │    │                                                                                                                    
  │  └─────────────────────────────────────────────┘    │                                                                                                                    
  └─────────────────────────────────────────────────────┘ 

Compaction — The Heart of LSM

When L0 has too many files (slow reads, overlapping ranges) → compaction merges and sorts them into L1.

BEFORE COMPACTION:

L0:  [SST-A: 1001/INFY@T1, 2005/INFY@T1]                                                                                                                                  
     [SST-B: 1001/INFY@T3, 1001/RELI@T4]   ← newer, overlaps SST-A
     [SST-C: 1001/TCS@T5(tombstone)]                                                                                                                                      

L1:  [SST-X: 1001/INFY@T0 → 2005/WIPRO@T0] ← old data, no overlaps

COMPACTION PROCESS:

Step 1: Read all L0 files + overlapping L1 files                                                                                                                          
Step 2: Merge-sort all KV pairs
Step 3: For same key, keep ONLY newest version:                                                                                                                           

    1001/"INFY"  appears in SST-A @T1 and SST-B @T3                                                                                                                         
                 → keep @T3 only, discard @T1                                                                                                                               

    1001/"TCS"   appears in SST-C @T5 as TOMBSTONE                                                                                                                          
                 → if no older versions survive → DROP ENTIRELY

Step 4: Write sorted output as new SST files in L1                                                                                                                        
Step 5: Delete old SST files (L0 files + old L1 file) 

AFTER COMPACTION:

 L0:  (empty)    

 L1:  [SST-NEW1: 1001/INFY@T3]  {qty:50}    ← T3 survived, T1 dropped                                                                                                      
      [SST-NEW2: 1001/RELI@T4]  {qty:80}
      [SST-NEW3: 2005/INFY@T1]  {qty:200}                                                                                                                                  
      [SST-NEW4: 2005/WIPRO@T0] {qty:75}                                                                                                                                   
      (1001/TCS is GONE — tombstone + old version both removed) 

Compaction is always read-old → merge → write-new. Always sequential I/O.

Read Path — How a Key Is Found

READ: SELECT qty WHERE user_id=1001 AND stock_symbol='INFY'
Target key: /Table/54/1/1001/"INFY"/0

 Step 1: Check MemTable (RAM)                                                                                                                                              
    ┌─────────────┐
    │  MemTable   │ ← binary search in skiplist                                                                                                                             
    │  has key?   │ → YES → return immediately (fastest path)                                                                                                               
    └─────────────┘                                                                                                                                                         
           │ NO                                                                                                                                                             
           ▼                                                                                                                                                                
  Step 2: Check Immutable MemTable (RAM, being flushed)                                                                                                                     
    ┌──────────────────────┐                                                                                                                                                
    │  Immutable MemTable  │ → YES → return                                                                                                                                 
    └──────────────────────┘                                                                                                                                                
           │ NO                                                                                                                                                             
           ▼                                                                                                                                                                
  Step 3: Check L0 files (newest first — they can overlap)
    ┌────────────────────────────────────────────────┐                                                                                                                      
    │  SST file 4 (newest L0)                        │
    │  Check bloom filter: "is 1001/INFY here?"      │                                                                                                                      
    │  Bloom says NO → skip entire file (no disk I/O)│                                                                                                                      
    └────────────────────────────────────────────────┘                                                                                                                      
    ┌────────────────────────────────────────────────┐                                                                                                                      
    │  SST file 3                                    │                                                                                                                      
    │  Check bloom filter: YES (maybe)               │                                                                                                                      
    │  Check footer: smallest=1001/HDFC,             │                                                                                                                      
    │                largest=1001/TCS                │                                                                                                                      
    │  1001/INFY falls in this range → check index   │                                                                                                                      
    │  Binary search index → find block offset       │                                                                                                                      
    │  Read 4KB block → find key → return value      │                                                                                                                      
    └────────────────────────────────────────────────┘                                                                                                                      
           │ NOT IN L0
           ▼                                                                                                                                                                
  Step 4: Check L1 (no overlaps → only ONE file can have this key)
    Check each SST footer: does range contain 1001/INFY?                                                                                                                    
    Binary search → find the one SST → bloom filter → read block                                                                                                            
           │ NOT IN L1                                                                                                                                                      
           ▼                                                                                                                                                                
  Step 5: L2, L3... until found                                                                                                                                             

  Worst case: MemTable miss + L0(all files) + L1 + L2 + L3 + L4 + L5                                                                                                        
              = many disk reads → this is LSM's read amplification problem

Bloom Filter — The Magic That Makes Reads Fast

Without bloom filters, every read would have to check every SST file. Bloom filters make this O(1) per file.

Bloom filter for SST file: A bit array of N bits, all starting at 0

INSERT key "1001/INFY":                                                                                                                                                   
    Hash with 3 different hash functions:                                                                                                                                   
      h1("1001/INFY") = 4   → set bit 4 = 1                                                                                                                                 
      h2("1001/INFY") = 17  → set bit 17 = 1                                                                                                                                
      h3("1001/INFY") = 31  → set bit 31 = 1                                                                                                                                

  Bit array:                                                                                                                                                                
    [0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1]                                                                                                       
             ▲                         ▲                           ▲                                                                                                     
            bit 4                    bit 17                      bit 31                                                                                                   

  LOOKUP key "2005/WIPRO" (not in this file):                                                                                                                               
    h1("2005/WIPRO") = 4   → bit 4 = 1 ✓                                                                                                                                    
    h2("2005/WIPRO") = 22  → bit 22 = 0 ✗ ← STOP                                                                                                                            
    Result: DEFINITELY NOT in this file → skip entire file                                                                                                                  

  LOOKUP key "1001/INFY":                                                                                                                                                   
    h1("1001/INFY") = 4   → bit 4 = 1 ✓                                                                                                                                     
    h2("1001/INFY") = 17  → bit 17 = 1 ✓                                                                                                                                    
    h3("1001/INFY") = 31  → bit 31 = 1 ✓                                                                                                                                    
    Result: PROBABLY in this file → go do actual disk read                                                                                                                  

  False positive: all 3 bits happen to be set by OTHER keys                                                                                                                 
                 → bloom says "maybe" → disk read → key not found                                                                                                           
                 → wasted one read, but correctness maintained                                                                                                              

  False negative: NEVER possible                                                                                                                                            
                 → if key is in file, its bits are always set 

Bloom filters live in RAM (~10 bits per key). 1 billion keys = ~1.25GB of bloom filters in RAM.

The Three Amplification Trade-offs

LSM makes a deliberate trade-off.

  1. Write Amplification
   One logical write → written multiple times during compaction                                                                                                            
    L0→L1→L2→L3 compaction = same data written 4 times                                                                                                                      
    Write amp factor: typically 10-30x                                                                                                                                      

    Example: INSERT one row                                                                                                                                                 
      1. Write to WAL                                                                                                                                                       
      2. Flush to L0 SST                                                                                                                                                    
      3. Compact into L1 SST
      4. Compact into L2 SST                                                                                                                                                
      ...         

    CRDB mitigates: compaction runs in background, doesn't block writes
  1. Read Amplification
One logical read → may check multiple SST files
    Worst case: check all levels
    Read amp factor: number of levels (typically 5-7)

CRDB mitigates: bloom filters skip most files instantly
 block cache keeps hot SSTs in RAM 
  1. Space Amplification
Old versions + tombstones exist until compaction cleans them                                                                                                            
A deleted row still takes space until GC + compaction                                                                                                                   
Space amp factor: typically 1.1x (10% overhead)

CRDB mitigates: MVCC GC runs after 25h TTL compaction drops tombstones
                Writes    Reads    Space
  B-Tree:        HIGH      LOW      LOW    ← random I/O writes                                                                                                              
  LSM:           MEDIUM   MEDIUM   MEDIUM  ← sequential everything 

메타데이터
post_id
3a5ec87dfae4
slug
lsm-trees-demystified-how-cockroachdb-and-rocksdb-store-your-data-3a5ec87dfae4
url
https://medium.com/@meenakshi_kumari/lsm-trees-demystified-how-cockroachdb-and-rocksdb-store-your-data-3a5ec87dfae4
canonical_url
https://medium.com/@meenakshi_kumari/lsm-trees-demystified-how-cockroachdb-and-rocksdb-store-your-data-3a5ec87dfae4
author_url
https://medium.com/@meenakshi_kumari
status
ok
fetched_at
2026-06-09 15:37:30