Skip to main content
Every Turbo pipeline has the same three building blocks (sources, transforms, and sinks), but how you arrange them determines how easy the pipeline is to operate, scale, and debug. This guide shows six patterns that cover most real-world pipelines, each with a complete, runnable example. Examples use Ethereum Mainnet (and Base for multi-chain). The same patterns work on any chain; see supported networks.

Choosing a pattern

Two decisions apply to every pattern:
  • Streaming or job mode: all examples below are streaming pipelines. For one-time backfills or exports, add job: true; see Job mode.
  • Resource size: each section suggests a starting resource_size. Start small and scale up if the pipeline lags; see the resource size reference for CPU and memory per tier.
Validate any pipeline before deploying it:

Linear pipeline

The simplest shape: one source, one or more chained transforms, one sink.
Use when you have a single data source, a single destination, and straightforward processing: decode, filter, reshape. This example decodes OrderFilled events from an exchange contract and writes clean, typed rows to PostgreSQL:
Resource size: s for filtered streams; move to m if you backfill a busy contract from earliest. Related pages:

Fan-out (one source, multiple sinks)

One source feeds multiple transforms, each writing a different view of the data to a different sink.
Use when different consumers need different views or subsets of the same data: for example, full history in a warehouse plus real-time alerts.
Default to one pipeline with multiple sinks when you send the same source data to several destinations. Sinks run independently: one failing does not block the others, and each can have its own batching settings. Splitting into one pipeline per destination duplicates source ingestion and wastes resources. Split only when the destinations need different resource sizes or genuinely independent lifecycles.
This example reshapes USDC transfers for an analytics warehouse and sends high-value transfers of any token to an alerting webhook:
Resource size: m; multiple sinks mean more concurrent work than a linear pipeline. Related pages:

Fan-in (multiple inputs, one sink)

Multiple event types are decoded from the same source, normalized to a common schema, and combined with UNION ALL into a single sink.
Use when you want a unified table combining several event types (trades, deposits, withdrawals, transfers) as one activity feed. This example builds a WETH activity feed from four event types. Every branch projects the same columns in the same order, which UNION ALL requires:
Resource size: l; decoding plus many transforms is the heaviest shape in this guide.

Multi-chain fan-in

The same shape works with multiple sources instead of multiple event types: combine chains into one output with UNION ALL.
See the multi-chain recipe in the cookbook for the complete pipeline. Use m for two chains and l as you add more. If the chains do not need to land in the same table, prefer the templated deployment below.

Multi-chain templated deployment

When you need the same pipeline logic across multiple chains, deploy one pipeline file per chain rather than a single multi-source pipeline.
Use when you want per-chain independence:
  • Independent lifecycle: deploy, pause, or delete one chain without touching the others
  • Independent checkpointing: one chain failing or lagging doesn’t block the others
  • Clearer monitoring: each chain has its own pipeline status and logs
The pattern is a template: write the pipeline once, then copy it and swap the chain-specific values. Here’s the Ethereum version:
ethereum-transfers.yaml
To create the Base version, copy the file and swap five values:

Templated vs. multi-source

Resource size: m per chain. Because each chain runs in its own pipeline, you can also size each one independently: a busy chain can run l while a quiet one runs s.

Dynamic table architecture

Dynamic tables give a pipeline runtime-updatable lookup data, the Turbo answer to “no joins in streaming SQL”. A dynamic_table transform is backed by a table (typically PostgreSQL) that you can update at any time without restarting the pipeline, and SQL transforms query it with the dynamic_table_check() function.

Pattern: dynamic allowlist or blocklist

The SQL transform filters records against the dynamic table. Change the table contents externally (from a backend, an admin tool, or plain SQL) and the pipeline picks up the change within seconds. Use when the set of things you filter on (wallets, contracts, tokens) changes over time, or is too large for a hardcoded WHERE ... IN list. This example only keeps transfers that touch a tracked wallet:
Add a wallet from any Postgres client, and the pipeline starts matching it within seconds:

Pattern: lookup enrichment

Store lookup data (token allowlists, protocol contract sets, factory-created pool addresses) in the dynamic table and reference it from transforms. A dynamic table can even populate itself from pipeline data using an inline sql: query; see the factory pattern example.

Choosing a backend

Use Postgres for production: it persists across restarts and can be updated externally.

Sizing considerations

  • Dynamic tables add memory overhead proportional to table size, so for large lookup tables (over ~100K rows) use the Postgres backend.
  • Lookups are batched and indexed, but cost still scales with table size, so keep tables as small as your use case allows.
  • Resource size: s is usually enough for allowlist filtering; size for your transform complexity, not for the dynamic table itself.
For full configuration syntax, table management, and more examples, see Dynamic tables and the dynamic_table_check reference.

PostgreSQL aggregate sink pattern

The postgres_aggregate sink maintains real-time running aggregations (balances, counters, totals) using a two-table pattern: a landing table receives raw events, and a database trigger incrementally updates an aggregation table.
Use when you need running totals updated on every event, without recomputing aggregates from scratch. Your application reads the small aggregation table instead of scanning event history. This example turns every ERC-20 transfer into two balance changes (negative for the sender, positive for the recipient) and lets the trigger maintain per-account, per-token balances:
Query the result like any Postgres table:
Supported aggregation functions: sum, count, avg, min, max. Not every function supports every operation type: sum and avg cannot handle updates, and min/max are insert-only. See supported aggregation functions before choosing.
This example tracks every token and holder on Ethereum Mainnet, which is a high write volume for a Postgres database. For production, add a WHERE clause to track specific tokens, or size your database accordingly.
Resource size: m. Throughput is usually bounded by the Postgres database rather than the pipeline, so scale the database before scaling the pipeline. For landing-table deduplication, trigger internals, and update/delete semantics, see the PostgreSQL aggregation sink reference.

Next steps