Backpressure in Akka Streams: Build High‑Throughput Pipelines Without Manual Throttling
Akka Streams’ backpressure automatically throttles fast sources when downstream stages slow, eliminating manual flow control. Learn how to build a high‑throughput pipeline with a concrete Flow example, buffer strategies, and actionable best‑practice tips.
19 Feb 2026, 09:30 UTC

Problem: “I need a fast stream but downstream is slow”
When you pull data from a fast source—say, a Kafka topic or a file—into a sink that writes to an external API, you often hit a bottleneck. The sink can only handle a few requests per second, but the source keeps emitting millions of messages per minute. The classic “slow consumer” problem can lead to buffer overflows, data loss, or even application crashes if you try to buffer everything.
The Akka Streams Backpressure Engine
Akka Streams solves this by propagating a request signal from downstream to upstream. Think of it as a handshake: the sink says “I can accept N more elements”; the upstream only sends that many. The built‑in backpressure is automatic—no manual throttling code required.
Key concepts:
- Source produces elements.
- Flow transforms elements.
- Sink consumes elements.
- Materializer runs the graph and manages buffers.
When a stage is slow, the materializer reduces the request size upstream, causing the upstream to pause or drop elements based on the configured OverflowStrategy.
Building a Flow with mapAsync and Backpressure
The mapAsync stage lets you process elements concurrently while preserving order. It’s ideal for CPU‑bound transformations or calls to external services that return futures.
import akka.actor.ActorSystem
import akka.stream.scaladsl._
import akka.stream.{ActorMaterializer, OverflowStrategy}
import scala.concurrent.duration._
implicit val system = ActorSystem("BackpressureDemo")
implicit val mat = ActorMaterializer()
import system.dispatcher
// 1. Source: 1,000,000 integers
val source = Source(1 to 1_000_000)
// 2. Flow: CPU‑bound map + async call
val compute = Flow[Int]
.mapAsync(parallelism = 8) { i =>
// Simulate expensive calculation
val result = i * i
// Simulate async external call
akka.pattern.after(10.millis, system.scheduler)(scala.concurrent.Future.successful(result))
}
// 3. Sink: artificially slow consumer
val slowSink = Sink.foreach[Int] { i =>
Thread.sleep(5) // 5 ms per element → ~200 elements/sec
}
// 4. Assemble graph
val graph = source
.buffer(256, OverflowStrategy.dropHead) // buffer size & strategy
.via(compute)
.to(slowSink)
// 5. Run
graph.run()
In this example:
- The
bufferstage holds up to 256 elements. When the buffer is full and the downstream is still slow, theOverflowStrategy.dropHeaddiscards the oldest element. - The
mapAsyncstage runs 8 concurrent futures. This keeps the CPU busy but still respects backpressure. - Because the sink sleeps 5 ms per element, the stream automatically throttles the source to about 200 elements/sec.
Buffer Size & Overflow Strategy: The Stability Trade‑off
Choosing the right buffer size and overflow strategy is critical. The table below summarizes common options and their impact.
| Strategy | Behavior | When to Use |
|---|---|---|
| DropHead | Discard oldest element when buffer full. | When you can tolerate data loss and want to keep newest data. |
| DropTail | Discard newest element. | When you prefer to keep the earliest data. |
| Fail | Throw exception, stream fails. | When data loss is unacceptable; triggers supervision. |
| Keep | Block upstream until space frees. | When you must process every element but can afford backpressure to propagate all the way to the source. |
Risk: Using DropHead or DropTail silently discards data—ensure a retry or idempotent design if data loss is unacceptable.
Bridging Akka Streams and Typed Actors
Actors can participate in streams via Source.actorRef and Sink.actorRef. This lets an actor emit or consume stream elements while still benefiting from backpressure.
val actorSource = Source.actorRef[Int](bufferSize = 100, OverflowStrategy.fail)
val actorSink = Sink.actorRef[Int](ref = myActor, onCompleteMessage = "done")
When the actor is slow, the stream’s buffer will apply the chosen strategy, preventing the actor from being flooded.
Monitoring Backpressure with .print and Akka Streams UI
The .print method prints a visual graph to the console, showing the flow of elements and buffer sizes. For example:
graph.print("Backpressure Demo")
Akka Streams UI (available in Akka HTTP) offers a web dashboard to observe stage metrics, including inCount, outCount, and bufferSize. Use it to confirm that backpressure is propagating as expected.
Trade‑offs & Limitations
- Buffer Size: Too small and you’ll see frequent drops; too large and you risk high memory usage.
- Ordering:
mapAsyncpreserves order but can become a bottleneck if the async call is slow. UsemapAsyncUnorderedif order is not critical. - Supervision: Restarting a stage on failure resets state. Prefer
ResumeorStopfor idempotent operations. - Actor Integration: Using
Source.actorRefwith a large buffer can hide upstream backpressure until the buffer is full.
Actionable Take‑aways
- Start with a reasonable buffer (e.g., 256) and
OverflowStrategy.keepto let backpressure propagate all the way to the source. - Use
mapAsyncfor concurrent processing; tuneparallelismto match your CPU cores. - Monitor with
.printduring development, then enable Akka Streams UI for production. - Choose
DropHeadorDropTailonly if you have a downstream retry or idempotent handling strategy. - When integrating actors, keep the buffer small and handle
OverflowStrategy.failto surface backpressure issues early.
By leveraging Akka Streams’ built‑in backpressure, you can build high‑throughput pipelines that automatically adapt to downstream speed, reduce boilerplate, and improve system resilience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.