Elixir's GenStage Demand, a visual explainer

lawik2 pts0 comments

Elixir's GenStage Demand (a Visual Explainer) – Andrea LeopardiElixir's GenStage Demand (a Visual Explainer)<br>August 7, 2026I’ve wanted to write this post for a long time, so here it goes. GenStage is probably my favorite Elixir abstraction. Its core idea—a pipeline of stages doing data processing—is neither particularly complicated nor novel, but the concept of demand is what makes me love it so much.

GenStage also embodies the beautiful and the ugly that Elixir systems can be. GenStage (and Elixir at large) is great for embarrassingly parallel problems, but it’s the codification of the main pitfall of these concurrent systems: you can’t get away with unbounded.

So, come with me through this lil’ journey of discovering how GenStage works, why it works like it does, and how elegantly it solves that nasty unboundedness deal.

The Problem

Let’s start with the core of the problem we’re looking at here: you have a bunch of data to shove through a process pipeline. To anchor this in reality, let’s take a real-world, common example: you want to consume messages from a queue (be it AWS SQS, or Apache Kafka, or whatever floats your boat).

Here is a small system with two parts:

A producer which can send between 1 and 12 events/second downstream (to the rest of the pipeline). This would be an Elixir process reading off of SQS/Kafka in our example.

A consumer process which can handle 4 events/second.

The “queue” between them can hold 12 events. That would be the message queue of the Elixir consumer process—12 events is a completely arbitrary limit for the sake of this example scenario, as in a real system the limit would be system memory.

Play around with events flowing from the producer to the consumer.

producer<br>Event source sending 1 /sec

consumer mailbox0/12

empty<br>consumer<br>Worker fixed at 4/sec

××××××××<br>events →<br>Producer speed1 event/sec1consumer limit: 4/sec12PauseReset

sent0<br>handled0<br>waiting0<br>lost0<br>Move the producer through 4 events/sec and keep going.

As you hopefully got a chance to see, at say a producer rate of 9 events/s, 5 events/s are left over . A queue with 12 free slots fills in a couple seconds. An unbounded queue lasts longer, but only because memory becomes the limit; at some point, the system poops out.

There’s a basic rule here: if work enters a system faster than it leaves for long enough, something inside the system must keep growing, reject work, or slow the input.

What this needs is a way for the consumer to limit what the producer sends.

Demand

Here’s where the concept of demand comes into play. Let’s add one rule: the producer may send only what the consumer asks for.

We’ll represent this “consumer wish” with the concept of demand. Demand is just a number; it expresses how many events the consumer wants. Demand of six means the consumer wants six events. The producer is only allowed to send up to “demand” events.

Send one request and watch all six permissions get used:

producer<br>Event source waiting for demand

ask +6<br>events received0/6

empty<br>consumer<br>Worker handles 2/sec

events →← demand<br>Ask for 6 eventsResetDemand left:0

requested0<br>sent0<br>handled0<br>The request must reach the producer before any event can move.

The key thing here to keep in mind: demand is a count . It doesn’t control “speed” in any way.

The consumer does not say “send n events per second”. It says “you may send<br>n more events”.

The playground component above has a deliberate flaw: it waits until all dispatched events are done before asking again. That keeps the queue bounded, but both processes spend time waiting because production is not instant:

The producer waits while the consumer handles events.

The consumer waits while the producer fetches the next batch .

This is the BEAM. The hell are we doing. The producer and consumer are separate processes. The producer could fetch the next batch of events while the consumer handles the events it already has!

Parallelization Station

The consumer should ask again when it is “almost ready”, not only when it has no work left.

Here are two runs with the same consumer and the same fetch time. Both start with six events. The only change is when the next request is sent:

producerfetch +6<br>consumer123456idle789

1.4 sec idle Fetching starts after event 6 finishes.

producerfetch +4<br>consumer1234567891011

0 sec idle The producer fetches while events 5–6 are handled.

PauseReplayelapsed 0.0 s<br>Same consumer. Same fetch time. Only the refill point changes.

In the first run, the consumer waits until it has processed all events before asking for more. Fetching begins “late”, so the consumer sits idle for a couple of seconds (that is, the time it takes for the producer to fetch n more events).

In the second run, the consumer asks for four more events (demand = 4) when two “units of demand” remain (that is, four events were consumed). The producer still spends a couple of seconds fetching, but in that time the consumer is handling those last two events in parallel....

events consumer producer demand elixir genstage

Related Articles