Definition
- Backpressure – The automatic flow-control mechanism by which a slow downstream operator causes upstream operators and the source to slow their production rate.
- Credit-based flow control – Since Flink 1.5, each receiving subtask advertises free buffer credits to senders, and a sender may only transmit as many buffers as the receiver has credits for.
- No data loss – Records are never dropped; with a replayable source like Kafka, backpressure means the consumer offset advances more slowly and lag grows temporarily.
- No configuration – The mechanism is always active as an emergent property of finite buffers; there is nothing to enable.
- Symptom, not cause – Backpressure tells you something is slow; the engineering response is to find and fix the bottleneck, not suppress the signal.
Imagine the National Bank of India has an assembly line of clerks passing transaction slips down a series of trays. If the last clerk is slow, their in-tray fills up. Once full, the clerk before them has nowhere to put new slips, so their out-tray fills too. Eventually the very first clerk, who takes slips from the incoming mail sack, simply stops pulling new mail because they have nowhere to put it. Nothing is thrown away; the whole line automatically slows to match the slowest clerk, and when that clerk catches up, everyone speeds back up.
Understanding the Concepts
What is backpressure in Flink?
Backpressure is the natural flow-control mechanism in Flink that prevents a fast producer from overwhelming a slow consumer. When a downstream operator cannot keep up, the pressure propagates backwards through the pipeline to the source, which slows its intake. No records are ever lost.
package com.bank.streaming;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class BackpressureDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.addSource(new KafkaSource<>("nbi-transactions"))
.keyBy(Transaction::getAccountId)
.process(new FraudScorer()) // fast
.addSink(new SlowDatabaseSink()); // SLOW: bottleneck
// SlowDatabaseSink cannot keep up:
// 1. Sink's input buffers fill
// 2. FraudScorer's output buffers fill
// 3. Source reads fewer Kafka records
// 4. Kafka lag grows but NO DATA LOST
env.execute("NBI Backpressure Demo");
}
}
How does credit-based flow control work?
Since Flink 1.5, each receiving subtask advertises to each sending subtask how many buffers (credits) it currently has free. A sender may only transmit as many buffers as the receiver has credits for. When the receiver is slow, credits dry up and the sender blocks.
Credit-based flow control:
Sender (FraudScorer-0) Receiver (DatabaseSink-0)
───────────────────── ─────────────────────────
Has data to send Free buffers: 10
Advertises 10 credits
Sends 10 buffers ──────────────▶ Receives, processes slowly
Free buffers: 2
Advertises 2 credits
Sends 2 buffers ──────────────▶ Still processing
Free buffers: 0
Advertises 0 credits
BLOCKED (no credits) Still processing...
Cannot send more data Eventually frees 3 buffers
Advertises 3 credits
Resumes sending ──────────────▶ Normal flow resumes
// No configuration needed!
// Credit-based flow control is ALWAYS active
// It is an emergent property of finite buffers
// You do NOT enable or disable it
How does backpressure propagate through the pipeline?
The pressure propagates hop by hop. When the sink is slow, its buffers fill, it advertises zero credits to the operator before it, that operator's output buffers fill, and so on back to the source. The source, finding no buffers to write into, reads fewer records from Kafka.
package com.bank.streaming;
public class PropagationDemo {
// Pipeline: Source -> Enricher -> Scorer -> Sink
//
// Sink is slow (database writes take 50ms each)
//
// Propagation:
// Step 1: Sink buffers fill (0 credits to Scorer)
// Step 2: Scorer output buffers fill (0 credits to Enricher)
// Step 3: Enricher output buffers fill (0 credits to Source)
// Step 4: Source stops reading from Kafka
//
// Kafka lag grows but records stay safely in Kafka
// When sink catches up, credits flow back and pipeline resumes
}
Backpressure propagation:
Kafka ──▶ Source ──▶ Enricher ──▶ Scorer ──▶ Sink (SLOW)
│
◀──── pressure propagates ◀────────┘
│
Source slows Kafka consumption
Kafka lag grows but data is safe
How do you diagnose backpressure?
The Flink Web UI shows per-operator backpressure status. The key metrics are busyTimeMsPerSecond (how much time the operator spends doing work), backPressuredTimeMsPerSecond (how much time it spends waiting because downstream is full), and idleTimeMsPerSecond (how much time it sits idle).
Diagnosing with Web UI and metrics: Operator Busy BackPressured Idle Status ────────── ──── ───────────── ──── ────── KafkaSource 10% 85% 5% Backpressured Enricher 15% 80% 5% Backpressured FraudScorer 20% 75% 5% Backpressured DatabaseSink 95% 0% 5% BOTTLENECK Reading: DatabaseSink is 95% busy, 0% backpressured --> It IS the bottleneck (busy but not waiting on anyone) Everything upstream is mostly backpressured --> They are WAITING on the sink
// Metrics to monitor via Prometheus/Grafana:
// flink_taskmanager_job_task_busyTimeMsPerSecond
// flink_taskmanager_job_task_backPressuredTimeMsPerSecond
// flink_taskmanager_job_task_idleTimeMsPerSecond
// flink_taskmanager_job_task_numRecordsInPerSecond
// flink_taskmanager_job_task_numRecordsOutPerSecond
// kafka_consumer_records_lag_max
What causes backpressure and how do you fix it?
Common causes include a slow external sink, heavy computation in an operator, data skew overloading one subtask, and insufficient parallelism. Each cause has a different fix.
package com.bank.streaming;
public class BackpressureFixes {
// Cause 1: Slow database sink
// Fix: batch writes, increase sink parallelism, use async writes
// .addSink(new BatchingDatabaseSink(batchSize=100))
// .setParallelism(4);
// Cause 2: Heavy computation (e.g., ML model scoring)
// Fix: increase operator parallelism, optimize model
// .process(new FraudScorer()).setParallelism(8);
// Cause 3: Data skew on keyBy
// Fix: salt keys, pre-aggregate, rebalance before keyBy
// stream.keyBy(txn -> txn.getAccountId() + "-" + txn.hash() % 4)
// Cause 4: Insufficient parallelism
// Fix: increase parallelism and add more TaskManagers
// env.setParallelism(16); // add more slots to cluster
}
Backpressure fix decision tree:
Is one operator 95%+ busy while upstream is backpressured?
YES --> that operator is the bottleneck
Is it a sink? --> batch writes, more parallelism
Is it a computation? --> optimize or increase parallelism
Is one subtask busy
while others idle? --> key skew: salt keys
NO --> check for external system issues
How does backpressure interact with checkpointing?
Backpressure slows barrier propagation because barriers travel through the same buffers as data. If buffers are full, barriers are delayed, which lengthens checkpoint duration. This is why Flink introduced unaligned checkpoints.
package com.bank.streaming;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.environment.CheckpointConfig;
public class UnalignedCheckpointDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(30_000);
// Unaligned checkpoints: barriers can overtake data in buffers
// Checkpoints complete faster under backpressure
env.getCheckpointConfig().enableUnalignedCheckpoints();
// Trade-off: unaligned checkpoints are larger because
// they include in-flight buffer data in the snapshot
env.execute("NBI Unaligned Checkpoint Demo");
}
}
Aligned vs unaligned checkpoints under backpressure:
Aligned: Barrier waits behind data in full buffers
Checkpoint duration: 30s+ (slow under backpressure)
Unaligned: Barrier overtakes data in buffers
In-flight data snapshot with checkpoint
Checkpoint duration: 2-5s (fast even under backpressure)
Trade-off: larger checkpoint size
What are common mistakes when dealing with backpressure?
The most common mistake is trying to fix backpressure by increasing buffer sizes. This only delays the onset and does not fix the underlying throughput mismatch. Another mistake is suppressing backpressure by dropping records, which loses data.
// WRONG: increasing buffer sizes to "fix" backpressure
// This only delays the onset, does not fix the throughput mismatch
// taskmanager.memory.network.fraction: 0.3 (was 0.1)
// Result: buffers fill a bit later, but still fill eventually
// WRONG: dropping records to avoid backpressure
// stream.filter(txn -> shouldDrop(txn)) // data loss!
// CORRECT: fix the bottleneck
// 1. Identify the busy operator
// 2. Increase its parallelism, optimize its logic, or fix its sink
// 3. Backpressure clears naturally when throughput matches
What should a senior engineer know about backpressure?
A senior engineer should know that backpressure is healthy self-protection and the correct response is to diagnose and remove the bottleneck rather than suppress the signal. They should also know that network buffer metrics, Kafka consumer lag, and the busy/backpressured/idle time ratios together point to the root cause, and that frequent restarts quietly erode throughput.
package com.bank.streaming;
public class SeniorBackpressureChecklist {
// Senior engineer's backpressure checklist:
//
// 1. Check Web UI for per-operator backpressure status
// 2. Find the operator with high busy%, low backpressured%
// (this is the bottleneck)
// 3. Check for subtask skew (one subtask busy, others idle)
// 4. Check Kafka consumer lag trend
// 5. Correlate with checkpoint duration
// (if checkpoints slow, consider unaligned checkpoints)
// 6. Fix the bottleneck:
// - Slow sink: batch, async, more parallelism
// - Heavy compute: optimize, more parallelism
// - Key skew: salt keys, pre-aggregate
// 7. Verify: backpressure clears, lag drains, throughput matches
//
// NEVER: increase buffer sizes as a "fix"
// NEVER: drop records to avoid backpressure
// NEVER: ignore it (it will get worse)
}
Visual Overview
Credit-Based Backpressure Flow
Normal flow (no backpressure):
Source ──10 credits──▶ Enricher ──10 credits──▶ Sink
Source ──sends data──▶ Enricher ──sends data──▶ Sink
(credits available) (credits available)
Backpressure (sink slow):
Source Enricher Sink (SLOW)
│ │ │
│ │ │ buffers fill
│ │◀── 0 credits ────────│
│ │ output buffers fill │
│◀── 0 credits ────────│ │
│ stops reading Kafka │ │
│ │ │
│ Kafka lag grows │ │
│ but data is SAFE │ │
Recovery (sink catches up):
Source Enricher Sink
│ │ │ frees buffers
│ │◀── 5 credits ────────│
│ │ sends data ──────────▶│
│◀── 5 credits ────────│ │
│ resumes Kafka read │ │
│ │ │
│ Lag drains │ Normal throughput │
Diagnosing the Bottleneck:
┌────────────┬──────┬──────────────┬──────┐
│ Operator │ Busy │BackPressured │ Idle │
├────────────┼──────┼──────────────┼──────┤
│ Source │ 10% │ 85% │ 5% │ ◀── waiting
│ Enricher │ 15% │ 80% │ 5% │ ◀── waiting
│ Scorer │ 20% │ 75% │ 5% │ ◀── waiting
│ Sink │ 95% │ 0% │ 5% │ ◀── BOTTLENECK
└────────────┴──────┴──────────────┴──────┘
Key Takeaway
Backpressure in Flink is the automatic flow-control mechanism that makes a pipeline self-regulate to the speed of its slowest stage. Since Flink 1.5, it uses credit-based flow control where each receiving subtask advertises free buffer credits to its senders. When a downstream operator is slow, its buffers fill, credits dry up, and the shortage propagates hop by hop back to the source, which reads fewer records from Kafka. No records are ever dropped, and no configuration is required.
The key insight is that backpressure is a symptom, not a disease. When you observe it, your job is to find the slow operator using the Web UI and metrics like busy time, backpressured time, and idle time. The operator with high busy percentage and zero backpressured percentage is the bottleneck. You fix it by increasing that operator's parallelism, optimizing its logic, batching writes to a slow sink, or fixing key skew.
At the National Bank of India, our transaction pipeline experienced backpressure during flash-sale traffic spikes. The database sink was the bottleneck, so the pressure propagated back to the Kafka source, which slowed consumption. The unconsumed transactions waited safely in Kafka as lag. We treated the backpressure as a signal, confirmed the bottleneck with metrics, and cured it by increasing the sink's parallelism and batching writes. We also enabled unaligned checkpoints because backpressure was lengthening checkpoint duration.
The rule is simple: backpressure is healthy self-protection, and the correct response is to diagnose and remove the bottleneck rather than suppress the signal.
Remember one sentence: Backpressure propagates through credit-based buffer flow control from a slow operator back to the source with no data loss and no configuration, and the fix is always to address the bottleneck.
---