Flink
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 20 min · 5 figures

    Flink and stream processing

    What an interviewer expects you to know about Apache Flink: dataflows and keyed state, event time and watermarks, windows, checkpoints and savepoints, end-to-end exactly-once with Kafka, hot keys and backpressure, and when to pick something else.

    Apache Flink is a distributed engine for stateful computation over streams. You write a dataflow of sources, operators and sinks; Flink runs it in parallel, keeps per-key state beside the code, tracks progress in event time with watermarks, and checkpoints everything consistently, so a crash neither loses nor double-counts state. It is the default interview answer to “aggregate, join or detect patterns in a Kafka stream within seconds”, provided you can say where its exactly-once ends.

    Picture it first: the festival order-counting room

    During Diwali week, a sweet-shop chain with 40 branches wants live totals of boxes sold per sweet, per hour. Slips carry the time of sale, but bike riders deliver them in bundles, late and out of order.

    A sorter routes each slip by sweet, so every kaju katli slip reaches clerk 3. Clerk 3 closes the 6–7 pm tally row not when the wall clock says 7, but when every rider’s latest slip shows 7:10, since no slip runs more than ten minutes behind. A 6:55 slip arriving at 7:40 goes into a “late” tray.

    Every fifteen minutes a bell rings: riders note their place, and clerks finish the slips ahead of the bell and photocopy their sheets. Totals are posted only after a round of copies is filed. If a clerk faints, everyone restarts from the copies and riders re-deliver from their noted places.

    Map it back: a clerk is a parallel subtask, routing by sweet is keyBy, the tally sheet is keyed state, the slip’s time is event time, the wall clock is processing time, “every latest slip shows 7:10” is a watermark, an hourly row is a window, the late tray is a side output, the bell is a checkpoint barrier, a rider’s place is a Kafka offset, the copies are a checkpoint, and posting only after filing is a transactional sink.

    Data model: a dataflow of parallel operators

    A Flink job is a directed graph: sources read streams (Kafka, database change logs, files), operators transform them, sinks write results. Streams are unbounded or bounded; batch is the bounded special case.

    Each operator runs as n parallel subtasks. Records move between them in three ways:

    • Forward: subtask i feeds subtask i, and Flink chains such operators into one task on one thread.
    • keyBy: hash the key, so any upstream subtask may send to any downstream one: a network shuffle that brings one key’s events together.
    • Rebalance and broadcast, for spreading load or shipping small rule sets.

    The JobManager schedules tasks, coordinates checkpoints and drives recovery; for high availability, standbys take over through ZooKeeper or Kubernetes leader election. TaskManagers are worker JVMs offering task slots, which reserve managed memory, not CPU. A slot can hold one slice of every operator, so a job needs as many slots as its highest parallelism.

    An ad-click counting job as you write it (top) and as it runs on two TaskManagers with two slots each (bottom).Notice Chaining turns four operators into two tasks per slot, and that the keyBy is the one place where records leave their slot and cross the network.

    Most new pipelines start in Flink SQL, which compiles to the same dataflows; the DataStream API gives you explicit state, timers and windows. Flink 2.0 (March 2025) removed the DataSet and Scala APIs.

    Time: event time, processing time and watermarks

    Processing time is the operator machine’s clock. It is simple, but results depend on arrival: a job running ten minutes behind files clicks into the wrong windows, and a replay gives a different answer. Event time is the timestamp in the record, when the click happened, so results depend only on the data and replays repeat; billing, fraud features and corrections need it. (Ingestion time, stamped at the source, is a rarely used middle ground.)

    Event time raises a new question: when is the 10:00–10:01 window complete? A phone in a metro tunnel uploads its clicks minutes late.

    Watermarks

    A watermark answers it. Watermark(t) flows in the stream and asserts that no more events with a timestamp ≤ t should arrive; when it passes a window’s end, the window fires.

    The usual generator is bounded out-of-orderness: highest timestamp seen − a bound, emitted every 200 ms by default. The bound is your latency-versus-completeness dial: 5 s fires each window 5 s after it ends and calls anything later late; 2 minutes catches more stragglers but delays every result by 2 minutes.

    Each source subtask (with Kafka, each partition) produces its own watermark, and an operator with several inputs takes the minimum. That keeps results correct, and it is why one silent partition can freeze a whole job.

    Clicks for one ad, plotted by when they arrive (across) and when they happened (up), with a 5 s out-of-orderness watermark and an allowed lateness of zero.Notice The 10:00:09 click arrives after the 10:00:12 click and still counts, and that the 10:00:51 click arrives behind a watermark that has already closed its window, so it is late.

    Late data

    An event behind the watermark is late and, with the default allowed lateness of 0, dropped. Your options: allowed lateness (keep window state longer; each straggler re-fires the window, so sinks must overwrite), a side output for anything later still, or a larger bound, which delays every window.

    The Dataflow Model: what, where, when, how

    Flink’s event-time design follows Google’s Dataflow Model (Akidau et al., VLDB 2015), which splits a streaming computation into four questions: what is computed (the aggregation), where in event time (the windows), when in processing time results are emitted (triggers: at the watermark, plus early or late firings), and how successive results relate (discarding, accumulating, or accumulating and retracting). Its argument: watermarks are sometimes too fast (late data still arrives) and sometimes too slow (one lagging input holds everything back), so triggers and refinements, not a perfect watermark, are how you trade correctness against latency and cost. Flink’s default trigger plus allowed lateness is the accumulating case.

    Windows

    Flink’s built-in window assigners:

    Window Shape Each event lands in Typical use
    Tumbling Fixed size, no overlap (1 min) 1 window Counts per minute, billing buckets
    Sliding (hopping) A size and a slide (1 h, every 1 min) size ÷ slide windows (60) Moving averages, “trending in the last hour”
    Session Ends after a gap of inactivity 1 session; sessions merge Visits, rides, browsing sessions
    Global One per key, forever 1 Only with a custom trigger
    The same seven clicks on one ad, grouped by 2-minute tumbling windows, 2-minute windows sliding every minute, and session windows with a 1-minute gap.Notice The tumbling windows split one burst of activity at the 2:00 boundary while the session window keeps it whole, and that every click appears in two sliding windows.

    A window is created by its first event, fires when the watermark passes its end, and is deleted once the watermark passes its end plus the allowed lateness. Prefer incremental functions (reduce, aggregate), which keep one accumulator per window instead of every event. The classic interview job, clicks per ad per minute (Flink 2.x):

    OutputTag<Click> lateTag = new OutputTag<>("late-clicks") {};
    
    SingleOutputStreamOperator<AdMinuteCount> counts = env
        .fromSource(kafkaClicks,
            WatermarkStrategy.<Click>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                .withTimestampAssigner((click, ts) -> click.clickTimeMillis())
                .withIdleness(Duration.ofMinutes(1)),             // quiet partitions must not stall time
            "clicks")
        .keyBy(Click::adId)
        .window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
        .allowedLateness(Duration.ofMinutes(1))
        .sideOutputLateData(lateTag)
        .aggregate(new CountClicks(), new AttachWindowStart())  // one accumulator per ad per window
        .uid("ad-minute-counts");                                 // stable ID for savepoints
    
    counts.getSideOutput(lateTag).sinkTo(lateClicksSink);

    Follow a click on ad 42 stamped 10:00:58. Arriving with the watermark at 10:00:57, it is on time; the window fires once a click stamped 10:01:05 or later lifts the watermark to 10:01:00. Arriving at 10:01:40, with the watermark at 10:01:30, it is late but inside the allowed minute: the window re-fires with the count plus one and the sink overwrites (ad 42, 10:00). After the watermark passes 10:02:00, the click goes to the side output.

    The same job in Flink SQL, where the watermark is part of the table:

    CREATE TABLE clicks (
      ad_id      BIGINT,
      click_id   STRING,
      click_time TIMESTAMP(3),
      WATERMARK FOR click_time AS click_time - INTERVAL '5' SECOND
    ) WITH ('connector' = 'kafka', 'topic' = 'clicks', 'format' = 'json' /* plus brokers */);
    
    SELECT ad_id, window_start, COUNT(*) AS clicks
    FROM TABLE(TUMBLE(TABLE clicks, DESCRIPTOR(click_time), INTERVAL '1' MINUTE))
    GROUP BY ad_id, window_start, window_end;

    Sliding windows hide cost. Flink keeps one accumulator, or one copy of each event, per window an event belongs to. A 1-hour window sliding every minute updates 60 accumulators per click, and 50,000 active ads hold 50,000 × 60 = 3 million of them. For “trending in the last hour, refreshed every minute”, count in 1-minute tumbling windows and keep a rolling sum of the last 60 per key.

    State: keyed state, backends and key groups

    Keyed state (ValueState, ListState, MapState and friends) is scoped to the current key, like a local key-value store sharded exactly as the stream is, so every update is a local read-modify-write with no locks or remote calls. Timers let a key wake itself: “no payment 15 minutes after checkout, so alert.” Operator state belongs to a subtask (sources keep Kafka offsets there), and broadcast state puts the same small map, such as fraud rules, on every subtask.

    State backends

    Backend Live state sits Gain Pay
    HashMap (default) Objects on the JVM heap Fastest access Bounded by memory; GC pauses and big full snapshots
    Embedded RocksDB LSM tree on local disk State far beyond memory; incremental checkpoints (De)serialisation per access, about 10× slower on average; 2^31 bytes max per key and per value
    ForSt (2.0+, experimental in 2.3) Files on S3 or HDFS, local disk as cache State beyond local disk; light checkpoints, fast rescaling Remote latency, hidden by async state access; not production-ready

    ForSt is Flink 2.0’s disaggregated state, aimed at containers with small disks and state in the hundreds of terabytes. In 2.0’s Nexmark runs, I/O-heavy queries with a 1 GB local cache reached 75–120% of local-state throughput.

    State TTL, and sizing state up front

    Unexpiring state grows forever. State TTL expires entries after a set time, though only in processing time, and costs 8 bytes per value on RocksDB. Flink SQL’s table.exec.state.ttl defaults to 0, meaning never: a streaming join or GROUP BY user_id keeps every key it has ever seen.

    Size state before the interviewer asks. Deduplicating clicks by click_id for 10 minutes at 50,000/s holds 50,000 × 600 = 30 million IDs; at an assumed 60 bytes each, about 1.8 GB across the job. A 24-hour window is 144 times that, about 260 GB: time for a Bloom filter or an idempotent sink instead.

    Key groups and max parallelism

    Flink hashes keys into key groups, as many as the operator’s max parallelism, and gives each subtask a contiguous range (as in the first figure). Rescaling moves whole key groups instead of rehashing keys. Max parallelism is the rescaling ceiling, and changing it discards state. The default is 128 for parallelism up to 128, otherwise min(nextPowerOfTwo(p + p/2), 32768). With 128, parallelism 4 gives each subtask 128 / 4 = 32 groups; 6 gives some 22 and some 21 (about 3% imbalance); 200 is impossible. If you expect growth, set it on day one, to a number with many divisors such as 720; its metadata cost grows linearly.

    Guarantees, precisely

    Checkpoints: barriers instead of stopping the world

    Flink’s fault tolerance is replay plus consistent snapshots, using asynchronous barrier snapshotting (Carbone et al., 2015), a variant of the Chandy–Lamport snapshot:

    1. The JobManager tells every source to start checkpoint n. Each source records its position (for Kafka, the offset per partition) and injects a barrier n into its output.
    2. Barriers flow with the records and never overtake them: records before barrier n belong to snapshot n, records after it to n + 1.
    3. An operator with several inputs aligns, holding each input whose barrier has arrived until barrier n arrives on all of them.
    4. It snapshots its state, forwards the barrier and carries on, while the state uploads asynchronously to S3 or HDFS.
    5. When every sink has acknowledged, checkpoint n is complete: source offsets plus matching operator state.

    Chandy–Lamport also records messages in flight on each channel. Because barriers travel in-band and inputs align, ABS needs only operator state on acyclic graphs; the replayable source stands in for the in-flight records.

    Checkpoint n in progress (top), and what happens after a TaskManager dies (bottom).Notice The snapshot holds Kafka offsets and operator state taken at the same logical point, so restoring both together is what makes replay safe.

    Recovery is the reverse: restart, load every operator’s state from the latest completed checkpoint, and seek each source to the offsets stored with it. Records after those offsets run again against state that has never seen them, so their effect counts once. (Offsets committed to Kafka are only for lag monitoring.)

    Three refinements matter at scale:

    • Unaligned checkpoints (since 1.11) let barriers overtake buffered records and save those buffers in the checkpoint, so checkpoints stay quick under backpressure, at the cost of more checkpoint I/O.
    • Incremental checkpoints (RocksDB, ForSt) upload only files changed since the last one: a 500 GB state that changes a few gigabytes uploads a few gigabytes.
    • At-least-once mode skips alignment for slightly lower latency; after a restore some records affect state twice.

    Checkpointing is off by default, and by default one failed checkpoint fails the job over.

    Savepoints

    A savepoint is a checkpoint you trigger by hand, in a portable format, that never expires: stop with a savepoint, then start new code, a new parallelism or a new Flink version from it. State is matched to operators by UID, so give every stateful operator a stable uid(...), or a refactor that changes the graph orphans its state. Even then, state is not guaranteed compatible from 1.x to 2.x.

    Exactly-once state is not exactly-once output

    Exactly-once in Flink means each event affects Flink’s state once, across failures. It does not mean each event is processed once: after a crash, records since the last checkpoint run through your code again, and anything they did outside Flink can happen twice. End to end you need a replayable source whose position is checkpointed, exactly-once checkpointing, and a sink that is transactional (two-phase commit tied to checkpoints) or idempotent (rewriting a result changes nothing).

    The Kafka sink is the standard two-phase commit. With DeliveryGuarantee.EXACTLY_ONCE it writes each interval’s output in a Kafka transaction, pre-commits when the barrier arrives and opens a new one, and commits only when the JobManager reports the checkpoint complete. A failure before that aborts the transaction and replays its input; a failure after completion retries the commit.

    Thirty seconds of a Flink job writing to Kafka with exactly-once delivery and a 10 s checkpoint interval, then crashing at 26 s.Notice Read_committed readers see each interval’s records only after its checkpoint completes, and never see the records of the aborted transaction.

    The price is latency: read_committed consumers (see Kafka) see output only after the commit, roughly one checkpoint interval plus checkpoint duration later. Also say:

    • The Kafka sink defaults to DeliveryGuarantee.NONE.
    • transaction.timeout.ms must exceed the longest checkpoint plus restart, or Kafka aborts a transaction Flink still means to commit; brokers cap it at 15 minutes by default.
    • Each job needs its own transactionalIdPrefix, or jobs fence each other.

    Idempotent sinks are often better for aggregates: upsert rows keyed by (ad_id, window_start) into Cassandra, ClickHouse or PostgreSQL. Replays rewrite the same values, results appear as soon as windows fire, and late firings overwrite, provided the output is deterministic.

    Exactly-once stops at external side effects

    An operator that sends an SMS, charges a card or calls an API from a map repeats the call for every record replayed after a failure. Put an idempotency key on the call (alert_id, payment_id), or emit an event and let an idempotent consumer act on it; see effectively-once with idempotent consumers.

    Choosing the checkpoint interval

    The interval trades steady overhead against replay after a failure and, with transactional sinks, output latency. Take 50,000 events/s into a job that can do 200,000 events/s when catching up:

    • 60 s interval. A crash just before a checkpoint replays 60 × 50,000 = 3 million events. Spare capacity is 200,000 − 50,000 = 150,000 events/s, so catching up takes 3,000,000 / 150,000 = 20 s, plus restore time; read_committed readers wait up to a minute.
    • 10 s interval. Replay 500,000 events, about 3.3 s of catch-up, and readers wait about 10 s, for six times as many checkpoints.

    With small state, 10–30 s is cheap; with hundreds of gigabytes, go incremental and keep checkpoint duration well below the interval. As Flink’s production checklist puts it, the interval is an SLA on how much you will reprocess.

    How it scales

    Parallelism and shuffles

    Scale by raising operator parallelism and adding TaskManagers, up to three ceilings. Source parallelism stops at the partition count; extra Kafka source subtasks sit idle and, without an idleness timeout, hold back the watermark. Keyed parallelism stops at max parallelism. And every keyBy serialises each record and usually ships it across the network, so filter and project first. Network buffers flush when full or every 100 ms, the knob between latency and throughput.

    Hot keys and skew

    keyBy sends every event for a key to one subtask. If one ad takes 30% of the clicks during an IPL final, its subtask does 30% of the work and its backpressure slows everything. Fixes, cheapest first:

    • Pre-aggregate before the shuffle and send partial counts; Flink SQL calls this local-global aggregation (mini-batch on).
    • Salt and aggregate twice: key by (ad_id, hash(click_id) % 8), then by ad_id to add the 8 partials.
    • For distinct counts, SQL’s split-distinct rewrite works the same way, or use HyperLogLog.

    Backpressure, and how to read it

    A slow subtask stops granting credits (one per free network buffer, since Flink 1.5), so its upstream stops sending, and the pressure travels back to the source, which simply reads Kafka more slowly: lag grows, nothing is dropped. Per subtask, busyTimeMsPerSecond, backPressuredTimeMsPerSecond and idleTimeMsPerSecond add up to about 1,000, and the UI marks a task HIGH when it is back-pressured over half the time. The bottleneck is the first busy task downstream of the back-pressured ones; those are victims, not culprits. See backpressure and load shedding.

    Rescaling

    Classic rescaling is stop with a savepoint, restart with a new parallelism. The adaptive scheduler rescales as slots come and go, reactive mode uses every TaskManager in the cluster, restoring from the latest checkpoint, and the Kubernetes Operator’s autoscaler sets each operator’s parallelism from its busy time and source backlog. Every rescale is still a restart, so huge state means slow scaling.

    Limits and numbers

    Defaults are from the Flink 2.3 documentation (2.3.0 shipped in June 2026; 1.20 remains the long-term-support 1.x line).

    What Number Notes
    Checkpointing Off by default Then: 10 min timeout, 1 in flight, 0 tolerated failures
    Incremental, unaligned checkpoints Both off by default Unaligned since 1.11, exactly-once mode only
    Max parallelism 128 if p ≤ 128, else min(nextPow2(1.5p), 32768) Rescaling ceiling; fixed once state exists
    Watermark emission, buffer flush 200 ms, 100 ms pipeline.auto-watermark-interval, execution.buffer-timeout.interval
    Allowed lateness 0 Late events are dropped
    State backend HashMap (heap) RocksDB: 2^31 bytes max per key or value
    State TTL Processing time only; +8 B per entry on RocksDB SQL table.exec.state.ttl = 0, keep forever
    Kafka sink guarantee NONE Broker transaction cap 15 min
    Flink 2.0 (March 2025) DataSet and Scala APIs removed; Java 11+ 1.x state not guaranteed to restore

    When to pick it, and when not

    Flink Kafka Streams Spark Structured Streaming Beam on Google Cloud Dataflow Consumer + Redis Streaming database
    Runs as A cluster running jobs A library in your service Micro-batches on Spark A managed service Your own loop A database (Materialize, RisingWave)
    Latency Per record; transactional output waits for checkpoints Per record; exactly-once commits every 100 ms About 100 ms; 1 ms continuous mode is experimental, at-least-once Per record; the service tunes workers Milliseconds Views updated as data arrives
    State Heap or RocksDB, checkpointed; ForSt remote RocksDB, backed by changelog topics Checkpointed state store Managed Redis; consistency is yours Inside the database
    Exactly-once State; end to end needs the right sink Kafka to Kafka With idempotent sinks Default for streaming Build it Internal; sinks vary
    Event time Watermarks, lateness, sessions, CEP, timers Windows with grace periods Watermarks, windows Richest triggers Build it SQL windows
    Pick it for Large state, event-time correctness, many sources Kafka-centric services with modest state Teams already on Spark Google Cloud with no ops Simple counters at low volume SQL users wanting live views
    Flink against the usual alternatives. Rows depend on configuration; the last row is the honest reason to pick each.

    Pick Flink when state is large or long-lived, event-time correctness with late data matters, you join or enrich several streams, or one job must cover both history and live data.

    Pick something else when it is Kafka in and Kafka out with modest state (Kafka Streams needs no cluster), minutes of latency are fine (a scheduled batch job; see batch vs stream processing), the work is stateless per event (a plain consumer group), or you need to serve queries: Flink computes, so write results to an OLAP store, Redis or a search index.

    Common interview usage

    Design problem Flink’s role Key and window Follow-up to expect
    Ad click aggregation Dedupe and count clicks into an OLAP store ad_id; 1-min tumbling; dedupe with 10-min TTL Late clicks; exactly-once counts
    Top-K trending Windowed counts, then a global top-K item_id, then one key; tumbling plus rolling sum Hot items; a count-min sketch for memory
    Fraud checks in a payment system Per-card state, timers, broadcast rules, CEP card_id; timers, not windows Latency budget; idempotent decisions
    Metrics and alerting Roll up samples, evaluate alert rules Metric and tags; tumbling Silence or a stalled watermark?
    Uber / food delivery Supply and demand per cell for surge; ETA features Geohash cell; sliding Hot city-centre cells; stale pings
    Change data capture Change log → enrich → search index or warehouse Primary key Per-row order; schema changes
    Web analytics Sessionise page views, count unique visitors visitor_id; 30-min session gap HyperLogLog for uniques
    Live leaderboard Aggregate score events into Redis sorted sets match_id Why not just ZINCRBY?
    Notification system Batch and dedupe into digests user_id; session window No double send after a restart

    In the interview

    When you draw Flink in a design, label the arrow into it with the key and the arrow out of it with the sink semantics: “keyed by ad_id” and “upsert by ad and minute”. Those two labels answer the first follow-ups.

    Going deeper: failure modes

    No output at all: a stalled watermark

    One idle Kafka partition (a quiet region at night, or more source subtasks than partitions) pins the minimum watermark, so no window fires while records still flow. Use withIdleness and alert on watermark lag. The reverse, one partition racing ahead in a backfill, bloats buffered state; watermark alignment throttles it.

    Late data vanishing quietly

    With allowed lateness 0, late events disappear and only the numLateRecordsDropped metric notices, yet offline phones can be hours late. Choose the bound, lateness and a side output deliberately, and reconcile money with a daily batch.

    State that grows until the job falls over

    Keys that never return (session or click IDs), global windows without a purging trigger, and SQL joins without a state TTL grow forever, slowing checkpoints and recovery until disks fill. Set TTLs, prefer windowed joins, and alert on checkpoint size.

    Checkpoints timing out under backpressure

    A slow sink fills buffers, barriers queue, checkpoints hit the 10-minute timeout, and the job restarts and falls further behind. Fix the bottleneck, enable unaligned checkpoints and buffer debloating (since 1.14), and tolerate a few failed checkpoints.

    Replays that change the answer

    Replay assumes the same input gives the same output. Wall-clock reads, random IDs or lookups against a changing table break that: an upsert sink may overwrite a correct row with a different one, and an at-least-once sink (a JDBC insert, Elasticsearch with generated IDs) duplicates everything since the last checkpoint. Derive IDs from the input and upsert on natural keys.

    Savepoints that won’t restore, and slow recovery

    Changing a UID, a state type or max parallelism makes old state unreadable, and forcing a start with --allowNonRestoredState silently drops it; set UIDs on day one and test restores in CI. Restoring terabytes after each failure also takes minutes: use incremental checkpoints, local recovery, more parallelism, or ForSt on 2.x.

    Cheat sheet

    Concept Remember it as
    Job Sources → operators → sinks; each operator has n subtasks
    keyBy Hash → key group → subtask; a network shuffle
    Watermark(t) “No more events ≤ t”; max seen − bound; minimum across inputs
    Late data Allowed lateness (default 0), then a side output
    Windows Tumbling: 1 per event; sliding: size ÷ slide per event; session: split by gaps
    State Keyed, local, lock-free; set a TTL; HashMap, RocksDB or ForSt
    Max parallelism Number of key groups; default 128; set once
    Checkpoint Barriers, aligned snapshots, source offsets; restore and rewind
    Savepoint Manual checkpoint for upgrades and rescaling; needs stable UIDs
    Exactly-once State only; end to end needs a transactional or idempotent sink
    Backpressure Credit-based; the culprit is the first busy task downstream

    How to say it in the interview

    “Between the clicks topic and ClickHouse I’d run a Flink job. It reads clicks in event time with a 10-second watermark bound, drops duplicate click IDs using keyed state with a 10-minute TTL, then keys by ad and counts one-minute tumbling windows, with a minute of allowed lateness and a side output for later clicks. RocksDB state is checkpointed to S3 every 30 seconds with the Kafka offsets, so a crash means restore and replay, not double counts. The sink upserts by ad and minute, so replays overwrite. If one ad runs hot, I pre-aggregate before the keyBy.”

    What does exactly-once mean in Flink? Each event affects Flink’s state once, because state and source offsets are snapshotted and restored together. Records still re-run after a failure, so output needs a transactional or idempotent sink, and side effects need idempotency keys.

    How does Flink know a window is complete? Watermarks: each source asserts “event time has reached t”, usually the highest timestamp minus a bound, and operators take the minimum across inputs. Stragglers within allowed lateness re-fire the window; later ones go to a side output.

    What happens when a TaskManager dies? The job restarts from the latest completed checkpoint: operators reload state, sources seek to the stored offsets, and up to one interval of input is replayed.

    One ad gets half the clicks. What breaks? Its subtask becomes the bottleneck and backpressure slows the whole job. Pre-aggregate before the shuffle, or salt the key and sum the partials in a second stage.

    Why not a consumer that increments Redis counters? Fine at low volume in processing time. It double-counts on replay, has no event-time windows and no consistent snapshot to rebuild from. Flink gives you those, and you pay with a cluster to run.

    Where to go next

    References and further reading

    1. Apache Flink 2.3 documentation: Stateful Stream Processingnightlies.apache.orgKeyed state and key groups, checkpoint barriers and alignment, unaligned checkpoints, recovery by replay, savepoints, and at-least-once mode.
    2. Apache Flink 2.3 documentation: Timely Stream Processingnightlies.apache.orgEvent time versus processing time, the definition of Watermark(t), the minimum across parallel inputs, and lateness.
    3. Apache Flink 2.3 documentation: Windowsnightlies.apache.orgWindow assigners, window lifecycle, allowed lateness (default 0), late firings, side outputs, and one copy of each element per window.
    4. Apache Flink 2.3 documentation: State Backendsnightlies.apache.orgHashMap (default) versus embedded RocksDB versus the experimental ForSt backend, incremental checkpoints and the 2^31-byte key/value limit.
    5. Apache Flink 2.3 documentation: Production Readiness Checklistnightlies.apache.orgThe default max-parallelism formula, why operator UIDs matter, checkpoint interval as an SLA, and JobManager high availability.
    6. Apache Flink 2.3 documentation: Kafka connectornightlies.apache.orgKafkaSink delivery guarantees (default NONE), transactions committed on checkpoint, transaction timeouts, idleness and offset commits.
    7. Apache Flink 2.0.0: A new Era of Real-Time Data Processing (Flink blog, March 2025)flink.apache.orgDisaggregated state with ForSt and the Nexmark results, plus the removed APIs and the 1.x-to-2.x state incompatibility.
    8. The Dataflow Model (Akidau et al., VLDB 2015)vldb.orgWhat, where, when and how; why watermarks are too fast or too slow; triggers and accumulation modes.
    9. Lightweight Asynchronous Snapshots for Distributed Dataflows (Carbone et al., 2015)arxiv.orgAsynchronous barrier snapshotting: a Chandy–Lamport variant that stores only operator state on acyclic graphs.
    10. Apache Spark 4.2 Structured Streaming Programming Guidespark.apache.orgMicro-batch latency of about 100 ms with exactly-once, and the still-experimental continuous mode (about 1 ms, at-least-once, map-like queries only).