Apache Flink — Internal Architecture

A developer's guide to how Flink actually works under the hood: the cluster runtime, how a program becomes parallel subtasks, how state lives and survives failures, and how checkpoints, watermarks, and backpressure keep results correct on an unbounded stream.

Student edition · understand, apply, verify

Understand recoverable stream state, event-time windows, and bounded flow control.

What you should be able to do: State source/sink assumptions and distinguish late events from windows whose cleanup horizon has passed.

Assumes basic programming and HTTP. Product defaults, formats, APIs, and guarantees depend on the deployed release/configuration. This is an architecture tutorial; pseudocode and local models are not production integrations.

Apache Flink is a distributed engine for stateful computation over data streams. Where a batch engine processes a finite dataset and stops, Flink treats data as an unbounded stream of events that may never end, and it keeps long-lived state as it goes. Its design rests on three reinforcing ideas: every operator can hold state that survives failures; results are computed in event time (when things happened) rather than the order events happen to arrive; and the combination of checkpoints and replayable sources gives exactly-once semantics for that state. Understanding Flink means understanding how those three pieces fit together on top of a fairly conventional master/worker runtime.

Contents

  1. Design Goals and Core Ideas
  2. Cluster Architecture
  3. The Dataflow Graph
  4. State & State Backends
  5. Checkpointing
  6. Savepoints vs Checkpoints
  7. Time & Windows
  8. Network Shuffle & Backpressure
  9. Summary
  10. Concepts explained
  11. Worked example
  12. Practice and verification

Concepts explained in this guide

Read these definitions first, or use the linked terms as you go. The examples and limitations are local to this guide; companion pages are optional.

Keyed state, checkpoints, and savepoints

Keyed state belongs to stable key groups. Checkpoints recover matching source positions and state; operator-managed savepoints support compatible planned changes.

Example. Restore a per-user counter with its Kafka offsets; rescale by moving key groups and matching operator UIDs.

Boundary. Source and sink contracts govern end-to-end effects. Savepoint backend/format/serializer compatibility matters; portable does not mean any version can restore.

Event time and watermarks

Event time is when the producer says an event happened; processing time is when a consumer handles it. A watermark is the processor's policy-driven estimate of event-time progress, used to emit and eventually clean up window state.

Example. An event for 12:00 arrives at 12:07. A 5-minute allowed-lateness policy may update its window, divert it to correction, or reject it according to the API contract.

Boundary. Watermarks are not proof all earlier events arrived. Idle inputs, skew, and unbounded delay require explicit policies; later corrections must propagate to rollups and reports.

Offsets and checkpoints

An offset is a position in one log partition. A checkpoint is a recoverable, consistent record of input positions and processing state. On restart, state must correspond exactly to the input that is considered processed.

Example. If a batch increments a total and crashes before saving its offset, replay increments twice. Commit the total, dedup state, and partition position together, or use a compatible transactional/idempotent sink.

Boundary. Saving an offset after a write avoids that specific loss window but can still duplicate; framework checkpoints alone do not make arbitrary external effects transactional.

Backpressure and load shedding

Backpressure tells an upstream producer that the downstream stage cannot accept more work. Buffering delays that signal; shedding deliberately rejects or drops lower-priority work when a bound is reached.

Example. A sink accepts 100 events/s while input is 150/s. A 1000-event queue fills in 20 seconds. The system must then block, reject, or apply its documented drop policy.

Boundary. A finite queue cannot absorb an infinite outage. Prioritized messages still need hard quotas, deadlines, and bounded client queues; otherwise an exemption can exhaust the system.

Cardinality

The number of distinct identities a store must track. A metric series is its metric name plus the full label combination. The product of label-value counts is an upper bound when not every combination occurs.

Example. 100 hosts × 20 routes × 5 status codes can produce up to 10,000 series. Adding a unique request_id can make every request create a new series.

Boundary. Hashing every ID to a distinct digest does not reduce cardinality or guarantee anonymity. Use bounded categories, explicit limits, or intentional fixed-bucket aggregation.

1. Design Goals and Core Ideas

Every internal decision in Flink traces back to a small set of goals for processing unbounded streams correctly. Keeping them in mind makes the rest of the architecture predictable.

GoalHow Flink achieves it
Stateful stream processingOperators keep partitioned, fault-tolerant state in a state backend. State is a first-class citizen, not a side effect bolted on top.
Event-time correctnessRecords carry their own timestamps. Watermarks track progress in event time, so results do not depend on the wall-clock order in which events arrive.
Exactly-once semanticsPeriodic checkpoints snapshot a globally consistent cut of all state. On failure the job rewinds to the last checkpoint and replays from there, so each event affects state exactly once.
High throughput, low latencyPipelined, record-at-a-time execution (not micro-batching) with buffered network transfer. Backpressure is handled natively.
ScalabilityEvery operator runs as many parallel subtasks. Parallelism is set per operator, and state is repartitioned cleanly on rescale via savepoints.
Flink design goals
An unbounded stream flows through stateful operators driven by an event-time engine; checkpointing and backpressure keep the results correct and the pipeline stable.
A useful mental model: Flink is a stream processor first and a batch processor second. Batch is treated as the special case of a bounded stream — the same runtime, with the knowledge that the input eventually ends.

2. Cluster Architecture

A Flink cluster follows a master/worker pattern. The master is the JobManager and the workers are TaskManagers. Unlike Cassandra's peer-to-peer ring, there is a clear coordination role — but it is split into three distinct responsibilities so that a single failed job cannot take down the whole cluster.

Flink cluster architecture
The JobManager splits into Dispatcher, ResourceManager, and a per-job JobMaster. TaskManagers expose task slots into which subtasks are deployed.

The components are:

A slot allocates configured resources/managed memory and can share operators; it is not universal CPU/OS-process isolation.

Submitting a job proceeds roughly like this:

function submit(job_graph):
  dispatcher.receive(job_graph)            # REST entry point
  jm = dispatcher.start_job_master(job_graph)
  needed = jm.compute_required_slots()     # from operator parallelism
  slots = resource_manager.allocate(needed)
  exec_graph = jm.build_execution_graph(job_graph)
  for subtask in exec_graph.subtasks():
    slot = slots.pick_for(subtask)
    task_manager(slot).deploy(subtask)     # ship code + state handle
  jm.track_status()                        # reschedule on failure

3. The Dataflow Graph

A Flink program is a logical description of operators and the streams connecting them. Before it runs, that description passes through three representations, each lower-level than the last.

StreamGraph to JobGraph to ExecutionGraph
The program becomes a StreamGraph, then a chained JobGraph sent to the JobManager, then a parallel ExecutionGraph of subtasks deployed onto TaskManagers.

Edges between operators come in two flavors. A forward edge keeps a record on the same subtask (and enables chaining). A redistributing edge — produced by keyBy, a rebalance, or a change in parallelism — sends records across the network to a different subtask. A keyBy in particular partitions the stream by a hash of the key so that all records for a given key always reach the same downstream subtask, which is what makes keyed state possible.

function chain(stream_graph):              # client side
  for op in stream_graph.operators():
    if op.input_is_forward() and op.parallelism == op.input.parallelism \
       and chaining_enabled(op):
      fuse(op, op.input)                   # one task, no network
  return job_graph                         # chained operators

function parallelize(job_graph):           # JobMaster side
  for task in job_graph.tasks():
    for i in range(task.parallelism):
      execution_graph.add_subtask(task, index=i)
  return execution_graph

4. State and State Backends

State is what separates Flink from a stateless stream filter. Anything an operator needs to remember between events — a running count, the contents of an open window, the last seen value per key — is held as Flink-managed state so the runtime can snapshot, restore, and repartition it. There are two kinds.

Keyed vs operator state and backends
Keyed state is partitioned by key and redistributed on rescale; operator state belongs to a subtask. Two backends trade speed for capacity.
KindScopeTypical use
Keyed stateBound to a single key on a keyed stream. Each key has its own independent value; a subtask only sees the keys hashed to it.Per-user counters, per-key aggregations, window contents. ValueState, ListState, MapState.
Operator stateBound to a parallel subtask, not a key. Redistributed by an explicit scheme when parallelism changes.Source connector bookkeeping — e.g. a Kafka source storing the read offset per partition.

Where that state physically lives is the job of the state backend. The choice is purely about capacity and access cost; it does not change semantics.

BackendWhere state livesBest for
HashMapStateBackendAs live Java objects on the JVM heap.State that fits in memory. Fastest access (no serialization), but bounded by heap size and subject to GC pressure.
EmbeddedRocksDBStateBackendSerialized in an embedded RocksDB instance on the local disk of each TaskManager.Very large state, larger than memory. Pays serialization cost per access but supports incremental checkpoints (only changed SSTables are uploaded).

Keyed state is the reason a keyBy matters so much: because every record for a key always lands on the same subtask, that subtask can keep the key's state locally with no coordination. When the job is rescaled, keys are reassigned to subtasks using key groups (fixed-size buckets of the key space), so state moves in whole buckets rather than key by key.

5. Checkpointing

Checkpoint barriers identify a consistent recovery cut of source positions and managed state. Aligned checkpoints coordinate input barriers; asynchronous snapshots can upload a captured state version while processing continues. Unaligned modes also capture required in-flight channel state. Exact mechanics depend on checkpoint mode and connectors.

Do not acknowledge a durable checkpoint handle before the required snapshot upload/metadata completion succeeds. Recovery restores the completed state/source positions, then replays; physical processing may happen again, while the logical recovered state reflects the correct prefix.

End-to-end effects need replayable retained sources and compatible idempotent/transactional sink commits. Checkpoints do not roll back arbitrary HTTP calls, emails, or uncoordinated database writes. They may align/stall inputs; 'without stopping any stream' is not an absolute performance guarantee.

6. Savepoints vs Checkpoints

Checkpoints are runtime recovery snapshots with a configured lifecycle; savepoints are user-managed snapshots for compatible planned changes. Formats can be canonical/native and depend on backend/release. Portable does not mean any serializer, backend, maximum parallelism, or application version can restore.

Use stable operator UIDs, compatible state schemas/serializers, supported key-group/max-parallelism settings, and an explicitly tested rescale/upgrade/rollback procedure. Stop/drain choices can affect timers, final windows, and side effects. Retain required state until restore is verified.

7. Time and Windows

Event timestamps assign records to windows under the chosen source clock/policy. Watermarks estimate event-time progress; they are not proof all prior events arrived. Multiple inputs, idle sources, skew, and out-of-order data need explicit watermark policy.

Whether a late record is still accepted depends on the target window's end, trigger, allowed lateness, and cleanup horizon. A timestamp below the watermark may still belong to a window that has not expired. Do not universally side-output every event_time≤watermark. Define half-open window boundaries and any correction/retraction behavior.

Keep retained source data, bounded state, and downstream correction semantics aligned. Missing/late telemetry is different from a zero count. Processing-time execution can be useful but may produce different replay results.

8. Network Shuffle and Backpressure

When subtasks are connected by a redistributing edge, records cross the network from a producer subtask to a consumer subtask. Flink moves them in buffers (batches of serialized records) rather than one record per packet, for efficiency. The interesting part is what happens when the consumer cannot keep up.

Network shuffle and credit-based backpressure
Data flows forward in network buffers; credits flow backward. A consumer only ever receives as many buffers as it has announced it can hold.

Flink uses credit-based flow control to handle this natively, with no explicit throttling logic in user code:

Credit-based channel buffers are bounded, but sustained pressure can grow source lag beyond retention or hit external limits. Backpressure does not guarantee no end-to-end loss during an unbounded outage.

function producer_send(channel, buffer):
  if channel.credits == 0:
    block()                              # backpressure: cannot send
  else:
    channel.transfer(buffer)
    channel.credits -= 1

function consumer_loop():
  while True:
    buf = input_gate.next_buffer()       # blocks if none ready
    process(buf)
    recycle(buf)
    announce_credit(buf.channel, +1)     # I have a free buffer again
Because backpressure flows all the way back to the source, a sustained backlog is visible at the source's intake rate and in Flink's per-task backpressure metrics. That makes the bottleneck operator easy to spot — it is the last one that is busy while everything upstream of it is blocked.

Worked example: late does not always mean discard

Work through the following bounded example and its failure boundary.

late does not always mean discardAssign event time 5 to window [0,10): Arrival order does not change its window.. Advance watermark to 6: An event at timestamp 5 is late relative to progress, but its window ending at 10 may not have fired/expired.. Apply the window's lateness policy: Accept/update, side-output, or reject according to window end, trigger, cleanup horizon, and allowed lateness—not a universal event_time≤watermark test.. Recover from a checkpoint: Restore matching state/source positions. External effects still need a compatible idempotent/transactional sink and retained input.1Assign event time 5 to window [0,10)2Advance watermark to 63Apply the window's lateness policy4Recover from a checkpoint
Sequence for this example. The table explains the evidence and decision at each step.
StepEvidence, decision, and expected result
Assign event time 5 to window [0,10)Arrival order does not change its window.
Advance watermark to 6An event at timestamp 5 is late relative to progress, but its window ending at 10 may not have fired/expired.
Apply the window's lateness policyAccept/update, side-output, or reject according to window end, trigger, cleanup horizon, and allowed lateness—not a universal event_time≤watermark test.
Recover from a checkpointRestore matching state/source positions. External effects still need a compatible idempotent/transactional sink and retained input.
Check your understanding: Do checkpoints stop a remote email from being sent twice?

No. Exactly-once managed-state recovery does not automatically cover arbitrary side effects outside checkpoint/sink protocols.

Run and change the example

Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:

python3 guide_labs.py windows
python3 guide_labs.py test

The model tests window-end cleanup versus raw timestamp and checkpoint/state replay.

The program tests its stated finite contracts. It is an educational model, not an implementation of the named production product or a proof of unmodeled distributed faults.

Sources and further verification

9. Summary

Flink's design is a small set of ideas that compose into correct, fault-tolerant stream processing:

ConcernMechanism
Who coordinates the cluster?JobManager split into Dispatcher, ResourceManager, and a per-job JobMaster; TaskManagers provide task slots.
How does a program become work?StreamGraph → chained JobGraph → parallel ExecutionGraph of subtasks deployed into slots.
What does an operator remember?Keyed state (per key) and operator state (per subtask), held in a HashMap or RocksDB state backend.
How does state survive failure?Chandy-Lamport barriers snapshot a consistent cut to durable storage; recovery rewinds all operators to the last checkpoint.
How are results correct?Exactly-once over state, plus replayable sources and transactional/idempotent sinks for end-to-end guarantees.
How do upgrades and rescaling work?Manual, portable savepoints; keyed state repartitioned by key group.
How is out-of-order data handled?Event time + watermarks; windows fire when the watermark passes their end; late data dropped or side-output.
How does a fast stage not swamp a slow one?Credit-based backpressure: consumers grant credits, producers only send what fits, pressure flows to the source.
The recurring theme: Flink keeps long-lived state as a first-class thing, reasons in event time rather than arrival order, and makes the whole pipeline consistent through barriers that flow with the data. Failure recovery, upgrades, and flow control are all built on those same primitives rather than bolted on.

⌂