Backpressure

pattern

Backpressure

Definition

Backpressure is a flow control mechanism in which downstream consumers communicate their processing capacity to upstream producers, preventing faster producers from overwhelming slower consumers. Without backpressure, a fast producer filling unbounded buffers will eventually exhaust available memory, crash the consumer process, or cause silent data loss through dropped messages. Backpressure transforms this failure mode into graceful load adaptation: when a consumer cannot keep up, the producer slows down automatically.

The term originates from fluid dynamics, where backpressure refers to the resistance that fluid encounters when flowing through a pipe. When a pipe narrows, pressure builds upstream, slowing the flow. In software systems, the "pipe" is a message queue or buffer, the "fluid" is data, and the "narrowing" is a slower consumer. Backpressure ensures that the software system behaves like a physical pipe rather than like an overflowing basin.

In the Elixir ecosystem, backpressure is not an afterthought or a library addition -- it is built into the core concurrency model through GenStage's demand-driven architecture. Consumers explicitly request a specific number of events from producers, and producers emit only what is demanded. This pull-based approach contrasts fundamentally with push-based systems (common in imperative languages) that rely on buffering, throttling, or hope to manage flow.

The Demand-Driven Model

GenStage's demand-driven model inverts the traditional producer-consumer relationship. Instead of producers pushing data to consumers, consumers pull data from producers by issuing demand:

Traditional (Push-Based):
  Producer ──[event]──> Buffer ──[event]──> Consumer
  (produces at          (grows              (processes at
   own rate)            unbounded)           own rate)

GenStage (Pull-Based):
  Producer <──[demand]── Consumer
  (emits only            (requests only
   what is               what it can
   demanded)             process)

The demand signal flows upstream (from consumer to producer), while events flow downstream (from producer to consumer). This bidirectional communication creates a self-regulating system where production rate automatically adapts to consumption capacity.

defmodule PrismaticOSINT.SignalProducer do
  @moduledoc """
  Produces OSINT signals from external sources.
  Only emits events when downstream consumers demand them.
  """
  use GenStage

  def start_link(opts) do
    GenStage.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl GenStage
  def init(opts) do
    source = Keyword.fetch!(opts, :source)
    {:producer, %{source: source, buffer: []}}
  end

  @impl GenStage
  def handle_demand(demand, %{buffer: buffer, source: source} = state)
      when demand > 0 do
    # Fetch only what is demanded, not more
    {to_emit, remaining} =
      case length(buffer) do
        n when n >= demand ->
          Enum.split(buffer, demand)

        _ ->
          # Need more data - fetch from source
          fresh = fetch_from_source(source, demand - length(buffer))
          combined = buffer ++ fresh
          Enum.split(combined, min(demand, length(combined)))
      end

    {:noreply, to_emit, %{state | buffer: remaining}}
  end

  defp fetch_from_source(source, count) do
    # Fetch exactly `count` items from the external source
    PrismaticOSINT.Source.fetch(source, limit: count)
  end
end

Producer-Consumer Patterns

GenStage defines three process roles that compose into data processing pipelines:

RoleBehaviorDemand DirectionEvent Direction
ProducerGenerates events on demandReceives demand from downstreamEmits events downstream
Producer-ConsumerTransforms events, passes demand throughReceives demand from downstream, issues demand upstreamReceives events upstream, emits events downstream
ConsumerProcesses events, generates demandIssues demand upstreamReceives events upstream

A typical pipeline chains these roles:

Producer ──> ProducerConsumer ──> ProducerConsumer ──> Consumer
(fetch)      (transform)         (enrich)            (store)
   ^              ^                   ^                  |
   |              |                   |                  |
   +──demand──────+──────demand───────+─────demand───────+

Demand propagates from right to left. The consumer requests N events from the enrichment stage, which requests N events from the transformation stage, which requests N events from the producer. Each stage only processes what was demanded, and the entire pipeline operates at the pace of the slowest stage.

defmodule PrismaticOSINT.SignalEnricher do
  @moduledoc """
  Producer-consumer that enriches raw OSINT signals with metadata.
  Passes demand upstream automatically via GenStage.
  """
  use GenStage

  @impl GenStage
  def init(opts) do
    {:producer_consumer, %{enrichment_config: opts[:config]}}
  end

  @impl GenStage
  def handle_events(signals, _from, state) do
    enriched =
      signals
      |> Enum.map(&enrich_signal(&1, state.enrichment_config))
      |> Enum.reject(&is_nil/1)

    {:noreply, enriched, state}
  end

  defp enrich_signal(signal, config) do
    with {:ok, geo} <- lookup_geolocation(signal),
         {:ok, reputation} <- lookup_reputation(signal, config) do
      Map.merge(signal, %{geolocation: geo, reputation: reputation})
    else
      {:error, _reason} -> nil
    end
  end
end

Buffer Management

While GenStage's demand model prevents unbounded buffering by design, real-world systems still need buffers to handle bursty workloads and asynchronous event arrival. GenStage provides configurable buffer management for producers:

Buffer ParameterDescriptionDefaultRecommendation
buffer_sizeMaximum events buffered when no demand exists10,000Size based on memory constraints
buffer_keepWhich events to keep when buffer is full (:first or :last):last:last for real-time data, :first for ordered processing
dispatcherHow events are distributed to multiple consumersDemandDispatcherPartitionDispatcher for parallel processing

When a producer receives events (from an external source, a callback, or a timer) but has no pending demand from downstream consumers, events are stored in the buffer. If the buffer reaches buffer_size, the overflow strategy applies: :last keeps the newest events (appropriate for real-time monitoring where stale data is less valuable), :first keeps the oldest events (appropriate for ordered processing where sequence matters).

Broadway: Production-Ready Backpressure

Broadway builds on GenStage to provide a higher-level abstraction for concurrent, multi-stage data pipelines with built-in backpressure. Where GenStage is a building block, Broadway is a framework that handles many production concerns automatically:

ConcernGenStageBroadway
BackpressureManual demand managementAutomatic
BatchingManual implementationBuilt-in configurable batching
ConcurrencyManual process spawningConfigurable processor/batcher pools
AcknowledgmentManualBuilt-in ack/reject for message sources
TelemetryManual instrumentationBuilt-in telemetry events
Graceful shutdownManual drain logicBuilt-in drain on shutdown

Broadway is the recommended approach for production OSINT pipelines in the Prismatic Platform, where reliable message processing with at-least-once delivery guarantees is essential.

Real-World OSINT Pipeline Example

The Prismatic Platform's OSINT intelligence gathering demonstrates backpressure in a real-world context. The pipeline processes signals from multiple external sources (Shodan, Censys, GreyNoise, DNS resolvers) through enrichment, validation, and storage stages.

The challenge: external sources can produce data at highly variable rates. A Shodan scan might return thousands of results in seconds, while a DNS resolution stage processes records one at a time with network latency. Without backpressure, the Shodan producer would buffer thousands of results, exhausting memory while the DNS stage processes them slowly.

External Sources          Processing Pipeline           Storage
+─────────+
| Shodan  |─────+
+─────────+     |    +───────────+   +──────────+   +─────────+
                ├───>| Normalize |──>| Enrich   |──>| Store   |
+─────────+     |    | (fast)    |   | (slow:   |   | (medium)|
| Censys  |─────+    +───────────+   | DNS,geo) |   +─────────+
+─────────+     |         ^          +──────────+        |
                |         |               ^              |
+─────────+     |         +──demand───────+──demand──────+
|GreyNoise|─────+
+─────────+

With backpressure:

  1. The Store consumer requests 50 events from Enrich
  2. Enrich requests 50 events from Normalize (it can batch efficiently)
  3. Normalize requests 50 events from the external source producers
  4. External source producers fetch exactly 50 records from their APIs
  5. When Store finishes processing 50 events, it requests 50 more
  6. The pipeline operates at the pace of the slowest stage (Enrich, due to DNS latency)

No stage accumulates unbounded data. No buffer grows without limit. Memory usage is predictable and stable regardless of how fast external sources can produce data.

Backpressure vs. Rate Limiting

Backpressure and rate limiting both control the rate of data flow, but they differ in mechanism and purpose:

DimensionBackpressureRate Limiting
DirectionConsumer controls producer (pull)System controls caller (push rejection)
MechanismDemand signals propagate upstreamRequest counter with time window
AdaptationContinuous, proportional to capacityBinary: allowed or rejected
Data lossNone (producer waits for demand)Rejected requests may be lost
Use caseInternal pipeline flow controlExternal API protection
Who benefitsBoth producer and consumerThe protected service

In the Prismatic Platform, both patterns are used at different boundaries. Backpressure controls flow within internal pipelines (GenStage/Broadway). Rate limiting controls access at external API boundaries (preventing abuse of the platform's endpoints). They are complementary, not competing, patterns.

Monitoring and Observability

Backpressure behavior is monitored through the platform's observability infrastructure. Key metrics include:

MetricIndicatesHealthy RangeAlert Threshold
Demand pendingConsumer appetite not yet satisfied0-100>1000 sustained
Buffer sizeEvents waiting for demand0-1000>5000
Processing latencyTime from event arrival to processing<100ms>1s
ThroughputEvents processed per secondApplication-specific<50% of baseline
Demand ratioDemand issued vs. events received~1.0<0.5 or >2.0

A sustained high buffer size with low demand indicates a bottleneck: the consumer cannot keep up, and the producer is buffering. A sustained low buffer size with high demand indicates underutilization: the consumer is waiting for data that the producer cannot supply fast enough. Both situations warrant investigation and potential pipeline reconfiguration.

Relationship to Stream Processing

Backpressure is one of three pillars of robust stream processing in the Prismatic Platform:

  1. Backpressure ensures flow control -- no stage is overwhelmed
  2. Fault tolerance ensures crash recovery -- failed stages are restarted by supervisors
  3. Circuit breakers ensure external boundary protection -- degraded dependencies are isolated

Together, these patterns create data pipelines that are resilient to load spikes (backpressure), internal failures (supervision), and external dependency degradation (circuit breakers). The combination is more robust than any individual pattern, and the Prismatic Platform applies all three in its production intelligence-gathering pipelines.

  • GenStage -- Elixir library implementing the demand-driven backpressure model
  • Broadway -- Production framework building on GenStage with built-in backpressure
  • Stream Processing -- Processing paradigm where backpressure is essential
  • Data Pipeline -- Architecture pattern using backpressure for flow control
  • Circuit Breaker -- Complementary pattern protecting external boundaries
  • Fault Tolerance -- System property maintained alongside backpressure
  • Supervisor -- Recovery mechanism for crashed pipeline stages
  • Rate Limiting -- Related pattern controlling external access rates
  • SEADF -- Platform framework implementing backpressure in scanning pipelines
  • OTP -- Runtime providing the process model for backpressure
  • Observability -- Monitoring infrastructure tracking backpressure metrics
  • Metrics -- Measurements used to monitor backpressure health

See Also


Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL