Exactly-Once with Flink Checkpoints

Data EngineeringAnalytics
Share on LinkedIn

"Exactly-once" gets thrown around in streaming pitches until the first duplicate charge hits production and someone discovers the JDBC sink wasn't idempotent. Flink's checkpoint mechanism is real and well-engineered — but end-to-end exactly-once is a system property, not a checkbox in StreamExecutionEnvironment.

Processing guarantees defined

Guarantee Meaning
At-most-once Records may be lost on failure
At-least-once Records may duplicate; none lost
Exactly-once Effect as if each record processed once

Flink's internal state updates can be exactly-once relative to checkpoint boundaries. External systems need matching semantics.

Checkpoints and barriers

Enable checkpointing:

env.enableCheckpointing(60_000); // every 60s
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30_000);
env.getCheckpointConfig().setTolerableCheckpointFailureNumber(3);

JobManager inserts barriers into streams. Operators snapshot state when barriers align across inputs (aligned checkpointing). Unaligned mode reduces backpressure latency at cost of larger state — Flink 1.11+ feature for high-lag scenarios.

State lands in configured state backend:

# flink-conf.yaml
state.backend: rocksdb
state.checkpoints.dir: s3://flink-checkpoints/prod/
state.savepoints.dir: s3://flink-savepoints/prod/
execution.checkpointing.externalized-checkpoint-retention: RETAIN_ON_CANCELLATION

RocksDB for large keyed state; heap for small jobs only.

Kafka source and offset commit

Flink Kafka source stores offsets in checkpoint state, not only Kafka consumer commits:

KafkaSource<String> source = KafkaSource.<String>builder()
    .setBootstrapServers("kafka:9092")
    .setTopics("orders")
    .setGroupId("flink-orders")
    .setStartingOffsets(OffsetsInitializer.committedOffsets())
    .setValueOnlyDeserializer(new SimpleStringSchema())
    .build();

On restore, source rewinds to checkpointed offsets — no duplicate reads beyond at-least-once window without transactional sinks.

Sink semantics matter

Kafka sink with two-phase commit:

KafkaSink<String> sink = KafkaSink.<String>builder()
    .setBootstrapServers("kafka:9092")
    .setRecordSerializer(...)
    .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
    .setTransactionalIdPrefix("flink-orders-")
    .build();

Transactional IDs tie to checkpoint IDs; aborted transactions roll back on failure.

JDBC / REST sinks — use upsert on primary key or outbox pattern. Naive insert duplicates.

Idempotent dedup — maintain processed (event_id) in Flink state or destination table:

stream
  .keyBy(Event::getEventId)
  .process(new KeyedProcessFunction<String, Event, Void>() {
    ValueState<Boolean> seen;
    // drop if eventId already in state
  });

End-to-end exactly-once checklist

  1. Source participates in checkpoint offset storage
  2. Operators use checkpointed state, no non-checkpointed side effects
  3. Sinks use 2PC or idempotent writes
  4. Side effects (email send, payment) use outbox or idempotency keys — Flink won't magically dedupe HTTP POSTs

Savepoints vs checkpoints

Checkpoints — automatic, recovery-focused, may delete after success.

Savepoints — user-triggered, portable, for upgrades and rescaling:

flink savepoint $JOB_ID s3://flink-savepoints/manual/
flink run -s s3://flink-savepoints/manual/savepoint-123 ...

Test savepoint restore in staging before Flink version bumps.

Operational tuning

Checkpoint duration exceeding interval causes backlog — increase interval, tune RocksDB incremental checkpoints, reduce state size with TTL:

StateTtlConfig ttl = StateTtlConfig
    .newBuilder(Duration.ofDays(7))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .build();

Monitor lastCheckpointDuration, numberOfFailedCheckpoints, alignment time. Chronic alignment blocking indicates skew — rebalance keys or isolate hot keys.

State size management

Unbounded keyed state kills checkpoint performance:

// Bad: state grows forever
stream.keyBy(Event::getUserId)
    .process(new KeyedProcessFunction<String, Event, Void>() {
        ListState<Event> allEvents;  // never cleared
    });

// Good: TTL on state
StateTtlConfig ttl = StateTtlConfig
    .newBuilder(Duration.ofDays(7))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .cleanupFullSnapshot()
    .build();

Monitor state size via Flink UI — rocksdb.estimated-num-keys metric. RocksDB incremental checkpoints help but don't eliminate large state cost.

Handling late and out-of-order events

Event-time processing with watermarks:

stream.assignTimestampsAndWatermarks(
    WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(10))
        .withTimestampAssigner((event, ts) -> event.getTimestamp())
)
.keyBy(Event::getOrderId)
.window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))
.allowedLateness(Duration.ofMinutes(1))
.process(new WindowProcessFunction<>() { ... });

Late events after watermark + allowed lateness go to side output — don't silently drop without monitoring side output volume.

Flink vs Kafka Streams choice

Factor Flink Kafka Streams
State size RocksDB, terabytes Local RocksDB, limited by disk
Checkpointing Chandy-Lamport barriers Offset-based
Operational complexity JobManager + TaskManagers Embedded in app
Exactly-once Checkpoint + 2PC sinks Transactional producer
Use case Complex event processing, large state Kafka-native transforms

Both achieve exactly-once with proper sink configuration. Flink for complex stateful processing; Kafka Streams when pipeline is Kafka-only and team wants fewer moving parts.

Failure modes

Production checklist

Flink state backend selection

Choose state backend based on state size and recovery requirements:

Backend State size Recovery speed Best for
HashMapStateBackend <100GB Fast (in-memory) Dev, small jobs
RocksDBStateBackend >100GB Slower (disk-based) Production, large state
ForStStateBackend >100GB Faster than RocksDB Flink 1.19+ production
# flink-conf.yaml
state.backend: rocksdb
state.backend.rocksdb.memory.managed: true
state.checkpoints.dir: s3://flink-checkpoints/
state.savepoints.dir: s3://flink-savepoints/
execution.checkpointing.interval: 60s
execution.checkpointing.mode: EXACTLY_ONCE

RocksDB with managed memory for production. HashMap only for development and small state (<10GB).

End-to-end exactly-once validation test

Prove exactly-once semantics before production:

def test_exactly_once():
    # 1. Produce N events with unique IDs
    produce_test_events(n=10000, topic="test-input")

    # 2. Run Flink job with intentional failure mid-checkpoint
    job = submit_flink_job()
    wait_for_checkpoint(job, n=2)
    kill_taskmanager(job)  # simulate failure
    wait_for_recovery(job)

    # 3. Verify output count == input count (no duplicates, no losses)
    output_count = count_sink_records("test-output")
    assert output_count == 10000, f"Expected 10000, got {output_count}"

Run after every Flink version upgrade and job logic change. Exactly-once is fragile — validate, don't assume.

Resources

Frequently asked questions

What is exactly-once processing in Flink?

Exactly-once means each record affects downstream state and sinks as if processed once, even across failures and restarts. Flink achieves this by coupling checkpointed operator state with transactional or idempotent sinks — end-to-end exactly-once also requires sink cooperation.

How do Flink checkpoints work?

JobManager injects barrier markers into streams. When an operator receives barriers from all inputs, it snapshots state asynchronously and acknowledges. On failure, Flink restores state from the latest completed checkpoint and replays sources from recorded offsets.

Does exactly-once in Flink guarantee no duplicates in Kafka output?

Only with two-phase commit sinks (Kafka transactional producer, JDBC XA) or idempotent upserts keyed by record ID. At-least-once processing plus non-idempotent sinks can duplicate on failure. Always validate end-to-end semantics, not just Flink's internal mode flag.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →