Checkpointing

Periodically saving processing state to enable recovery from failures without reprocessing all data from the beginning

TL;DR

Checkpointing is the process of periodically saving the processing state of a stream processing application, enabling it to resume from the last checkpoint rather than restart from scratch after failures. Essential for exactly-once semantics (per-partition, within a transaction) in systems like Apache Flink, Spark Streaming, and Kafka Streams.

Visual Overview

Checkpointing Overview
WITHOUT CHECKPOINTING (Restart from beginning)
┌────────────────────────────────────────────────┐
│  Stream Processing Job                         │
│                                                │
│  T0: Start processing from offset 0            │
│  T1: Processed 1,000,000 messages              │
│  T2: Processed 5,000,000 messages              │
│  T3: CRASH! ⚡                                  │
│  T4: Restart from offset 0 ✕                   │
│      → Reprocess all 5,000,000 messages        │
│      → Hours of lost work                      │
│      → Possible duplicate outputs              │
└────────────────────────────────────────────────┘

WITH CHECKPOINTING (Resume from last checkpoint)
┌────────────────────────────────────────────────┐
│  Stream Processing Job with Checkpoints        │
│                                                │
│  T0: Start, checkpoint offset=0                │
│  T1: Process 1M messages, checkpoint offset=1M │
│  T2: Process 4M more, checkpoint offset=5M     │
│  T3: CRASH! ⚡                                  │
│  T4: Restore from checkpoint ✓                 │
│      → Resume from offset=5M                   │
│      → Only reprocess since last checkpoint    │
│      → Fast recovery (seconds vs hours)        │
│      → No duplicates (exactly-once,            │
│        per-partition, within a transaction)    │
└────────────────────────────────────────────────┘

CHECKPOINT LIFECYCLE
┌────────────────────────────────────────────────┐
│  Checkpoint Coordinator                        │
│              ↓                                 │
│  1. Trigger Checkpoint                         │
│     Insert barrier into stream                 │
│              ↓                                 │
│  Stream: [msg1, msg2, BARRIER, msg3, msg4]     │
│              ↓                                 │
│  2. Operator receives barrier                  │
│     - Save current state to storage            │
│     - Save current offset                      │
│     - Acknowledge checkpoint                   │
│              ↓                                 │
│  3. All operators acknowledged?                │
│     YES → Checkpoint COMPLETE ✓                │
│     NO  → Wait or timeout                      │
│              ↓                                 │
│  4. Commit checkpoint metadata                 │
│     Store: {                                   │
│       checkpoint_id: 123                       │
│       timestamp: T                             │
│       state_location: s3://...                 │
│       offsets: {partition0: 5000, ...}         │
│     }                                          │
└────────────────────────────────────────────────┘

RECOVERY FLOW
┌────────────────────────────────────────────────┐
│  Job Failure Detected                          │
│              ↓                                 │
│  1. Find Latest Successful Checkpoint          │
│     checkpoint_id: 123                         │
│     timestamp: 2 minutes ago                   │
│              ↓                                 │
│  2. Restore Operator State                     │
│     Load state from: s3://checkpoint-123/      │
│     - Counters, windows, caches                │
│     - Application state                        │
│              ↓                                 │
│  3. Reset Stream Positions                     │
│     Kafka offsets: {partition0: 5000, ...}     │
│              ↓                                 │
│  4. Resume Processing                          │
│     Start from checkpoint offsets              │
│     Reprocess messages since checkpoint        │
│                                                │
│  Recovery Time Objective (RTO):                │
│  - Checkpoint interval: 1 minute               │
│  - State restore: 10 seconds                   │
│  - Reprocess: 1 minute of data                 │
│  Total: ~90 seconds downtime                   │
└────────────────────────────────────────────────┘

Core Explanation

What is Checkpointing?

Checkpointing is a fault-tolerance mechanism that periodically saves the state of a distributed computation to durable storage. For stream processing, this includes:

  1. Application State: Counters, aggregations, windows, caches
  2. Input Positions: Kafka offsets, file positions, database cursors
  3. Metadata: Checkpoint ID, timestamp, version

Key Property: Consistency

A checkpoint must capture a consistent snapshot across all parallel operators, ensuring:

  • No messages are lost (completeness)
  • No messages are duplicated (exactly-once, per-partition, within a transaction)
  • State is consistent across operators

Checkpoint Barriers (Chandy-Lamport Algorithm)

Stream processing systems use barriers to coordinate consistent snapshots without stopping the stream:

Checkpoint Barriers
How Barriers Work:

Source Operator injects barriers into stream:
┌────────────────────────────────────────┐
│  Kafka Topic                           │
│  [msg1, msg2, msg3, BARRIER_10, msg4]  │
│           ↓       ↓       ↓            │
│  Operator A processes messages         │
│  When BARRIER_10 arrives:              │
│  1. Save state (e.g., count=150)       │
│  2. Forward barrier downstream         │
│  3. Continue processing                │
└────────────────────────────────────────┘

Multiple Input Streams (Barrier Alignment):
┌────────────────────────────────────────┐
│  Stream 1: [msg1, BARRIER_10, msg2]    │
│  Stream 2: [msg3, msg4, BARRIER_10]    │
│           ↓              ↓             │
│  Join Operator                         │
│           ↓                            │
│  Wait for BARRIER_10 from BOTH streams │
│  (buffer messages from faster stream)  │
│           ↓                            │
│  All barriers received → Save state    │
└────────────────────────────────────────┘

Why Barriers?
✓ No need to pause the stream (live checkpointing)
✓ Consistent snapshot across distributed operators
✓ Minimal impact on throughput (less than 5% overhead)

Checkpoint Interval Trade-offs

Checkpoint Interval Trade-offs
Frequent Checkpoints (e.g., every 10 seconds):
Pros:
✓ Fast recovery (less data to reprocess)
✓ Small recovery window
Cons:
✗ Higher overhead (I/O, CPU for serialization)
✗ More storage costs
✗ Can slow down processing

Infrequent Checkpoints (e.g., every 10 minutes):
Pros:
✓ Lower overhead
✓ Better throughput
Cons:
✗ Slower recovery (more data to reprocess)
✗ Larger recovery window
✗ Higher risk of data loss

Recommended: 1-5 minutes for most applications

Checkpoint Storage

Where to Store Checkpoints:

Checkpoint Storage Options
1. Distributed File Systems:
 - HDFS, S3, GCS, Azure Blob
 - Pros: Scalable, durable, cost-effective
 - Cons: Higher latency (~100ms writes)
 - Use case: Production systems

2. Distributed Databases:
 - RocksDB (local + replicated)
 - Cassandra, DynamoDB
 - Pros: Fast writes (~10ms)
 - Cons: Higher cost, operational complexity
 - Use case: Low-latency requirements

3. In-Memory (with replication):
 - Redis, Memcached with persistence
 - Pros: Very fast (~1ms)
 - Cons: Limited capacity, expensive
 - Use case: Small state, ultra-low latency

Storage Structure:
checkpoint_dir/
├── checkpoint-000010/
│ ├── operator-state-0
│ ├── operator-state-1
│ └── metadata
├── checkpoint-000011/
│ └── ...
└── _metadata (latest checkpoint info)

Incremental Checkpointing

Incremental Checkpointing
Full Checkpoint (naive approach):
Every checkpoint saves entire state
State size: 10 GB
Checkpoint interval: 1 minute
I/O: 10 GB/minute = 166 MB/s ✕ (expensive!)

Incremental Checkpoint (RocksDB-based):
Only save changed state since last checkpoint
┌────────────────────────────────────────┐
│ Checkpoint 10: Save full state (10GB)  │
│ Checkpoint 11: Save delta (100MB)      │
│ Checkpoint 12: Save delta (150MB)      │
│ Checkpoint 13: Save delta (120MB)      │
│ ↓                                      │
│ Recovery: Restore checkpoint 10        │
│ + Apply deltas 11,12,13                │
└────────────────────────────────────────┘

Benefits:
✓ Reduce I/O by 10-100x
✓ Faster checkpoint completion
✓ Lower storage costs

Implementation: Apache Flink RocksDB state backend

Exactly-Once Semantics with Checkpointing

Exactly-Once Semantics
How Checkpointing Enables Exactly-Once:

1. Atomic Commits:
 ┌───────────────────────────────────────┐
 │  Process messages 1-100               │
 │  Update state (count += 100)          │
 │           ↓                           │
 │  Checkpoint:                          │
 │  - Save state: count=100              │
 │  - Save offset: 100                   │
 │  - Commit offset to Kafka (atomic)    │
 │           ↓                           │
 │  If crash before checkpoint:          │
 │  → Restore offset=0, count=0          │
 │  → Reprocess messages 1-100 ✓         │
 │                                       │
 │  If crash after checkpoint:           │
 │  → Restore offset=100, count=100      │
 │  → Skip messages 1-100 ✓              │
 └───────────────────────────────────────┘

2. Two-Phase Commit (for sinks):
 ┌───────────────────────────────────────┐
 │  Phase 1: Pre-commit                  │
 │  - Write to staging table             │
 │  - Don't make visible yet             │
 │           ↓                           │
 │  Checkpoint completes ✓               │
 │           ↓                           │
 │  Phase 2: Commit                      │
 │  - Move from staging to production    │
 │  - Make writes visible                │
 │                                       │
 │  If crash before Phase 2:             │
 │  → Staging writes discarded ✓         │
 │  → Reprocess and retry                │
 └───────────────────────────────────────┘

Real Systems Using Checkpointing

SystemCheckpoint MechanismDefault IntervalState BackendUse Case
Apache FlinkChandy-Lamport barriersDisabled (manual)RocksDB, HeapReal-time analytics, ETL
Spark Structured StreamingMicro-batch checkpointing5 secondsHDFS, S3Batch + streaming
Kafka StreamsState stores + offset commits30 secondsRocksDBStream processing
Apache StormRecord-level ackingN/A (acking-based)N/ALow-latency streaming
Apache SamzaChangelog-based1 minuteRocksDB + KafkaStateful streaming
Apache Flink Checkpointing
Flink Checkpoint Configuration:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// Enable checkpointing every 60 seconds
env.enableCheckpointing(60000);

// Checkpoint configuration
CheckpointConfig config = env.getCheckpointConfig();

// Exactly-once mode (vs at-least-once)
config.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);

// Minimum pause between checkpoints (prevent overload)
config.setMinPauseBetweenCheckpoints(30000);

// Checkpoint timeout (fail if takes too long)
config.setCheckpointTimeout(600000);

// Max concurrent checkpoints
config.setMaxConcurrentCheckpoints(1);

// Retain checkpoints on cancellation (for savepoints)
config.enableExternalizedCheckpoints(
CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION
);

// State backend (where to store state)
env.setStateBackend(new RocksDBStateBackend("s3://checkpoints/"));

Checkpoint Flow in Flink:
┌────────────────────────────────────────┐
│ 1. JobManager triggers checkpoint      │
│ - Increment checkpoint ID              │
│ - Send trigger to sources              │
│ ↓                                      │
│ 2. Sources inject barriers             │
│ - Insert barrier into stream           │
│ - Save current offset                  │
│ ↓                                      │
│ 3. Barriers flow through operators     │
│ - Each operator saves state            │
│ - Acknowledges checkpoint              │
│ ↓                                      │
│ 4. Sinks receive barriers              │
│ - Pre-commit external writes           │
│ - Acknowledge checkpoint               │
│ ↓                                      │
│ 5. JobManager collects acks            │
│ - All tasks acknowledged? ✓            │
│ - Commit checkpoint metadata           │
│ - Finalize external writes             │
└────────────────────────────────────────┘

Recovery in Flink:

1. Job fails (e.g., task exception, node crash)
2. JobManager restarts job from latest checkpoint
3. All tasks restore state from checkpoint
4. Sources reset to checkpoint offsets
5. Processing resumes (exactly-once guaranteed per-partition, within a transaction)

Case Study: Kafka Streams Checkpointing

Kafka Streams Checkpointing
Kafka Streams uses:
- Local RocksDB for state storage
- Kafka topics for changelog (state replication)
- Kafka offsets for position tracking

Properties config = new Properties();
config.put(StreamsConfig.APPLICATION_ID_CONFIG, "my-app");
config.put(StreamsConfig.STATE_DIR_CONFIG, "/tmp/kafka-streams");

// Commit interval (checkpoint frequency)
config.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 30000);

// State store configuration
config.put(StreamsConfig.STATE_CLEANUP_DELAY_MS_CONFIG, 600000);

How it works:
┌────────────────────────────────────────┐
│ State Changes:                         │
│ 1. Write to local RocksDB              │
│ 2. Write to changelog topic (Kafka)    │
│ 3. Commit offset every 30s             │
│ ↓                                      │
│ Recovery:                              │
│ 1. Restore RocksDB from changelog      │
│ 2. Resume from committed offset        │
│ 3. Replay uncommitted messages         │
└────────────────────────────────────────┘

Exactly-once in Kafka Streams (EOS):

- Transactional writes to output topics
- Atomic offset commits
- Changelog updates in same transaction
- Result: End-to-end exactly-once (per-partition, within a transaction)

When to Use Checkpointing

✓ Perfect Use Cases

Use CaseScenario / RequirementSolutionBenefit
Long-Running Stream JobsReal-time analytics running 24/7; needs no data loss, exactly-once (per-partition, within a transaction)Checkpoint every 1 minute to S3Recover from failures in under 2 minutes
Stateful AggregationsCounting events per user over 24-hour windows; state size 100 GBIncremental checkpointing with RocksDBPreserve state across restarts, avoid recomputation
Complex Event ProcessingMulti-stage pipeline with joins, enrichment; needs consistent state across operatorsDistributed checkpointing with barriersConsistent snapshots without pausing stream

✕ When NOT to Use (or Use Carefully)

Anti-PatternProblemAlternativeExample
Stateless ProcessingNo state to checkpoint, pure transformationJust reprocess from source (no checkpointing overhead)Filter, map, simple parsing
Ultra-Low Latency (< 1ms)Checkpointing adds latency (barrier alignment)At-least-once processing with deduplicationHigh-frequency trading, real-time bidding
Small Batch Jobs (< 1 minute)Checkpoint overhead > job durationJust rerun the job on failureScheduled micro-batch jobs

Interview Application

Common Interview Question

Q: “Design a real-time fraud detection system processing millions of transactions/second. How would you ensure exactly-once processing and fast recovery from failures?”

Strong Answer:

“I’d design a checkpointed stream processing system:

The architecture is Flink for exactly-once processing (per-partition, within a transaction), RocksDB for local keyed state, S3 for durable checkpoint storage, and a 1-minute checkpoint interval. The state is keyed by user_id and contains the latest profile, fraud-rule parameters, and 5-minute transaction windows; at 10M users × 5KB, I would plan for roughly 50 GB of managed state.

The checkpoint path needs to be cheap on the normal path and reliable on recovery. I would use incremental checkpoints, keep a full checkpoint hourly, take deltas every minute, and run asynchronous snapshots so checkpoint I/O does not block transaction processing. Chandy-Lamport barriers give a consistent snapshot across operators even when messages arrive out of order.

Exactly-once is an end-to-end contract: Kafka source offsets are part of the checkpoint, Flink runs in exactly-once mode, and the sink uses two-phase commit so outputs are only finalized after the checkpoint completes. On failure, the JobManager restores the latest checkpoint from S3, resets Kafka offsets to the checkpointed position, and replays roughly one minute of messages.

The target recovery time is about 90 seconds: 30 seconds to restore state, about 30 seconds to replay the checkpoint interval, and buffer for scheduling. I would monitor checkpoint duration, alignment time, state size, replay lag, and duplicate rate; checkpoint duration over 60 seconds would page the owning team.

The tuning trade-off is explicit. A 30-second interval improves RTO but may push overhead toward 8%; a 5-minute interval is cheaper but recovery can take 5-6 minutes. For fraud detection I would start at 1 minute, retain the last 10 checkpoints, and create manual savepoints before version upgrades.”

Code Example

Simple Checkpointing Implementation

import time
import json
import hashlib
from typing import Dict, Any
from pathlib import Path

class CheckpointManager:
    """
    Simple checkpointing system for stream processing
    """
    def __init__(self, checkpoint_dir: str, interval_seconds: int = 60):
        self.checkpoint_dir = Path(checkpoint_dir)
        self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
        self.interval_seconds = interval_seconds
        self.last_checkpoint_time = 0
        self.checkpoint_id = 0

    def should_checkpoint(self) -> bool:
        """Check if it's time to create a checkpoint"""
        return time.time() - self.last_checkpoint_time >= self.interval_seconds

    def create_checkpoint(self, state: Dict[str, Any], offset: int) -> int:
    # ... omitted: keep concept snippets short
    del processor  # "crash"

    processor_recovered = StreamProcessor('/tmp/checkpoints')
    print(f"Count of 'hello' after recovery: "
          f"{processor_recovered.get_word_count('hello')}")
    print(f"Current offset after recovery: "
          f"{processor_recovered.current_offset}")

    # Cleanup
    processor_recovered.checkpoint_manager.cleanup_old_checkpoints(keep_last=2)
from dataclasses import dataclass
from typing import Union, List
import queue

@dataclass
class Message:
    """Regular stream message"""
    data: str
    offset: int

@dataclass
class Barrier:
    """Checkpoint barrier"""
    checkpoint_id: int

class BarrierBuffer:
    """
    Buffer messages until all input barriers received
    (for operators with multiple inputs)
    """
    def __init__(self, num_inputs: int):
        self.num_inputs = num_inputs
    # ... omitted: keep concept snippets short
    operator.on_message(Barrier(checkpoint_id=1), input_id=0)

    # Stream 2 messages (delayed barrier)
    operator.on_message(Message("kafka", 0), input_id=1)
    operator.on_message(Message("flink", 1), input_id=1)

    # This completes the checkpoint
    operator.on_message(Barrier(checkpoint_id=1), input_id=1)

    print(f"Final state: {operator.state}")

Prerequisites:

Related Concepts:

Used In Systems:

  • Apache Flink: Distributed checkpointing with barriers
  • Spark Structured Streaming: Micro-batch checkpointing
  • Kafka Streams: State stores with changelog

Explained In Detail:

  • Stream Processing Deep Dive - Checkpointing implementation details

See It In Action

Quick Self-Check

  • Can explain checkpointing in 60 seconds?
  • Understand how checkpoint barriers work?
  • Know the trade-offs between checkpoint frequencies?
  • Can explain how checkpointing enables exactly-once semantics?
  • Understand incremental vs full checkpointing?
  • Can design a checkpoint strategy for given requirements?

Production signal

Why this concept matters

Interview 60% of streaming interviews
Production Flink, Spark, Kafka Streams
Performance Fast recovery
Scale Exactly-once semantics