Learn how to design explicit backpressure policies with core.async using bounded channels, dropping and sliding buffers, worker limits, and timeout-based overload handling.
Backpressure: A system’s way of saying, “the consumer cannot keep up, so the producer must slow down, buffer carefully, shed work, or fail fast.”
Backpressure is not an edge case. It is what turns an asynchronous design from “works in demos” into “survives real traffic.” If producers can outpace consumers indefinitely, the real result is usually hidden queue growth, latency spikes, memory pressure, and eventually timeouts or crashes.
The main design question is not whether backpressure exists. It is which policy you want when it appears.
Most Clojure async systems use one of these responses:
core.async gives you direct tools for each of these choices.
1(require '[clojure.core.async :as async :refer [chan]])
2
3(def synchronous-jobs (chan))
4(def bounded-jobs (chan 64))
5(def sampled-events (chan (async/dropping-buffer 100)))
6(def latest-snapshot (chan (async/sliding-buffer 1)))
These are not implementation details. They encode business policy:
A surprisingly large amount of backpressure design comes down to explicit worker count and bounded queues.
1(def in (chan 128))
2(def out (chan 128))
3
4(async/pipeline-blocking
5 8
6 out
7 (map expensive-blocking-step)
8 in)
The important parts here are:
Without those limits, “asynchronous” often just means “the failure is delayed.”
Use feedback when the producer can adapt. Use timeouts when waiting forever is worse than failing. Use shedding when the workload contains low-value items.
1(go
2 (let [[result source]
3 (async/alts! [results (async/timeout 200)])]
4 (if (= source results)
5 result
6 {:status :timed-out})))
Timeouts are part of backpressure policy. They are not just defensive programming. They decide how long the system will pretend the downstream is still healthy.
Backpressure is much easier to manage when you observe it directly:
If none of those are visible, overload is usually discovered too late.
Good backpressure design is concrete:
The visual below shows the most important distinction: either pressure travels back toward the producer early through bounded queues and explicit rejection, or it stays trapped in deep internal buffers until latency becomes the visible failure mode.
When that policy is explicit, the system becomes easier to operate and easier to explain.