← Back
Home › Systems › Data processing and pipelines

ingestion

Moves data from APIs, databases and files into a warehouse or lake on a schedule or as a change stream, loading it exactly once and in a queryable format.

Airbyte, Fivetran

Open the ingestion pageingestion Hdr histogramdata-structures: Log-linear bucketed histograms with bounded relative error for latency percentilesConsumer offsetsdistributed: Each consumer group stores one integer per partition, the next position to read, so a restarted consumer resumes where it left off with at-least-once delivery.›Dead-letter queuedistributed: A side queue for messages that keep failing: after N delivery attempts or a permanent error, park them with the reason so the main queue keeps moving.›Deterministic simulationtestingdistributed: Run the whole system in one thread under a seeded scheduler with simulated network, disk and clock, so every failure replays from its seed.›Primary-backup replicationdistributed: One node orders all writes and ships them to standbys; the acknowledgement rule (none, one, all) sets what a crash loses, and an external arbiter decides who takes over.›Idempotency keysfinance: Client-chosen unique key per logical operation, stored with a request fingerprint and the final response, so retries replay the first result instead of acting twice.›Process supervisionhardware-os: A parent process starts services, reaps their exits and restarts them under a delay and burst-limit policy, then stops them with SIGTERM before SIGKILL.›Cursor paginationnetworking: Keyset/opaque-cursor pagination for stable incremental reads from APIs and tablesCoverage-guided fuzzingsecurity: Mutate inputs, run an instrumented target, and keep any input that reaches a new code edge; feedback turns blind guessing into incremental search.›Mutual TLSsecurity: TLS where the client also proves a certificate key, so every connection carries a verified, rotatable machine identity.›OAuth 2 and OpenID Connectsecurity: Delegated authorization (OAuth 2) and login on top of it (OIDC); use authorization code + PKCE for every user-facing client.›Secrets managementsecurity: Keep keys and passwords out of code and logs, give workloads short-lived identity instead of static keys, and make rotation routine.›Change data capturestorage: Decode the database's own commit log into row-change events in commit order, with a resume position, so any consumer can mirror the data exactly.›Chunked backfillstorage: Process old data in bounded key ranges, one short transaction per chunk, with a saved checkpoint and idempotent writes, so a crash resumes and live traffic never waits.›Columnar file formatstorage: Row groups, column chunks, pages, encodings and footer stats (Parquet/ORC style) (listed in Moved to the wiki)Point-in-time recoverystorage: Periodic base backups plus a continuously archived change log let you rebuild the state as of any instant, then branch from there.›Schema evolutionstorage: Compatibility rules (backward/forward/full) for adding, removing and renaming fieldsUpsert mergestorage: MERGE/upsert by key with deduplication and late-arriving data rulesExponential backoff with jittersystems: Retry after delays that grow geometrically and are randomized, so failed clients spread out instead of hammering a recovering service in synchronized waves.›Graceful drainingsystems: On a termination signal, stop taking new work, finish in-flight work until a deadline, tell clients to reconnect elsewhere, then exit; clients resume from a token.›Token bucketsystems: A bucket of up to b tokens refilled at rate r; requests spend tokens, enforcing an average rate while allowing bursts of b.›Fencing tokensdistributed: A number that grows with every lock grant and is checked by the protected resource, so a paused ex-holder's late write is rejected.›Segmented logdistributed: Split an append-only log into files named by their first offset, write only the newest, and find any record by binary search plus a sparse index.›Cooperative schedulingconcurrency: Green threads/coroutines yielding at await points on a user-space schedulerFault injectiondistributed: Inject latency, errors, kills and partitions via proxies, netem or hooksRecord replaydistributed: Record every nondeterministic input (time, randomness, I/O, model and tool outputs, per-tick player input) and re-execute deterministically for debugging, replays and state rebuildHealth checkingsystems: Decide per backend whether to route to it using probes and real-traffic errors, with consecutive-count hysteresis, capped ejection and a panic fallback.›Leasesdistributed: A lock that expires on its own: bounded-time ownership that survives crashes and partitions, given bounded clock drift and fencing tokens on writes.›Message sequence numbersdistributed: A per-stream counter on every message lets the receiver detect loss, duplicates and reordering, ack cheaply and resume after reconnect with "send from N".›Cryptographic hashes (SHA-2,BLAKE3)security: Collision-resistant fingerprints of data; SHA-256 for interoperability, BLAKE3 for speed, HMAC/HKDF for keyed uses.›Code coverage instrumentationcompilers: Insert counters into compiled code to record which blocks, edges or lines ran; a spanning tree of the control-flow graph cuts the counters needed.›Delta debuggingcompilers: Ddmin minimization of failing inputs or change setsFast hash functionsdata-structures: Non-cryptographic hashes built from 64×64→128-bit multiplies and xor-shifts; pick by key size, trust level and whether the hash is persisted.›+ 12 moreTransparent interceptionnetworking: Redirect traffic to a local proxy (iptables TPROXY/REDIRECT, eBPF) without app changesSyscall interceptionsecurity: Intercept syscalls via ptrace, seccomp user notify or a user-space kernelControl-flow graphcompilers: Split a linear instruction list into basic blocks and connect them by jumps; the graph every optimizer, SSA builder and dataflow pass walks.›Minimum spanning treegraphs: Cheapest set of edges connecting all vertices; Kruskal sorts edges and joins components with union-find, Prim grows one tree, Boruvka merges in parallel rounds.›Failure bisectionsearch-sort: Binary search over commits, batches or inputs to isolate a failure+ 4 moreEbpfhardware-os: Verified in-kernel programs on hooks (kprobes, XDP, tc) with mapsPtrace process controlhardware-os: Ptrace: attach, read and write registers and memory, single-step, intercept signalsSeccomp bpfsecurity: Syscall filtering with seccomp BPF programsBFS and DFSgraphs: The two basic graph traversals; BFS gives hop distances and levels, DFS gives discovery/finish order for cycles, components and topological order.›d-ary heapdata-structures: Array heap with d children per node (d = 4 typical): half the binary heap's depth, siblings in one cache line, faster priority queues.›Union-findgraphs: Disjoint-set forest with union by size and path compression; merges sets and answers same-set queries in effectively constant time.›+ 3 moreSparse matrices (CSR)numerics: Compressed sparse row stores only nonzeros as row pointers, column indices and values; SpMV streams them once, so it is memory-bandwidth bound.›

How it is built

Architecture overview

A data ingestion pipeline copies data from systems you do not control (SaaS APIs, operational databases, files) into a store you query (a warehouse, a lake, a search index), repeatedly and without losing or duplicating rows. It is built from three kinds of process: extractors that talk to a source, a loader that writes to the target, and a state store that remembers how far each stream has been copied. The one idea that makes it work is the checkpoint: the extractor emits records and, now and then, a state token, persisted only after the loader has durably written every record before it. After a crash the extractor restarts from the last token, so records may arrive twice but never go missing (at-least-once delivery), and the loader makes duplicates harmless with keys and merges. Airbyte states this contract for incremental syncs as "at least once" replication (Airbyte docs, sync modes).

Parts:

 source system ──► connector ──records + state──► (optional broker) ──► staging files ──► loader ──► target tables
 (API / DB / file)   │  ▲                                              (batches)       (merge)       │
                     │  └── rate limit, retry, auth ──┐                                              │
                     └── change log / cursor / chunk ──┘            state store ◄── "all records before token T are written"
                                                                          ▲
                              scheduler ── start, retry, backfill ────────┘            dead-letter queue ◄── bad rows

How the parts fit together

Source connector: pagination, cursors and credentials

Most sources are HTTP APIs that return pages. Offset pages (?page=7) skip or repeat rows when data changes between requests; a cursor (an opaque token or a sort key such as (updated_at, id)) continues exactly where the previous page ended. That makes cursor-pagination (no page yet) the extraction primitive; the same word names the incremental marker, the stored "highest updated_at seen so far" (Airbyte docs). A timestamp cursor has a trap: rows with the same timestamp straddling a boundary. Use a composite sort key or re-read a small overlap and let the loader deduplicate.

Credentials come from two places. User-authorised SaaS access uses OAuth 2 and OpenID Connect: the connector holds a refresh token and exchanges it for short-lived access tokens (RFC 6749). Databases use passwords or keys. Both go through Secrets management: the connector gets a secret by reference at run time, never in the job definition or logs, and refreshed tokens are written back to it.

Connectors speak a narrow contract to the platform. In Airbyte's protocol a source implements spec, check, discover and read; read takes the catalog and previous state and emits record and state messages as JSON lines on standard output. Singer is similar (SCHEMA, RECORD, STATE).

Request governor: stay under the limit, retry politely

Source APIs rate-limit by key, endpoint or account and answer 429 or 5xx (RFC 6585, RFC 9110). A Token bucket per credential in front of the HTTP client spends the allowed rate smoothly and allows a small burst, which is cheaper than discovering the limit by being rejected. When a request still fails, Exponential backoff with jitter spaces the retries; the AWS Architecture Blog comparison found that jitter cuts client work and server load. Retrying a write needs more than backoff: the retry must carry the same key (see the loader).

Change capture and backfill

Polling a table by updated_at misses hard deletes and rows updated without touching the column, and loads the operational database. Change data capture reads the database's write-ahead log instead: PostgreSQL logical decoding turns the WAL into an ordered stream of row changes that a replication slot holds until the consumer has acknowledged it (PostgreSQL docs, logical decoding). Debezium is the common open-source reader; it uses that stream for PostgreSQL and the binlog for MySQL (Debezium architecture docs).

Two consequences shape the pipeline. First, the slot is the source-side checkpoint and keeps WAL on disk until the consumer confirms, so a stopped consumer fills the source disk; it needs an alert and a size cap (PostgreSQL: max_slot_wal_keep_size, default unlimited, and idle_replication_slot_timeout). Second, the log holds changes, not history, so the first load needs a snapshot, and a new table or a repaired bug needs a backfill. Chunked backfill copies history in bounded key ranges, each its own retryable unit, and runs beside the live change stream; the loader's merge by primary key resolves the overlap, because a row can legitimately arrive from both paths and the version with the higher log position wins.

State store: where the checkpoint lives

The state is a small map from stream (and partition) to position: a cursor value, a log sequence number or the last chunk finished. This is the idea behind Consumer offsets in a message broker, and the same rule applies: commit the position only after the work it covers is durable. Airbyte's destination contract says this directly: a destination that emits a state message is declaring that every record before it was written, so the state it carries is safe to persist. Committing early loses data on a crash; committing late only re-reads. Kafka Connect keeps source connector offsets in a compacted topic (offset.storage.topic) and can provide exactly-once delivery for source connectors that support it (Kafka Connect user guide).

State must be inspectable and resettable per stream: resetting it is how you re-sync after a bug, so the loader must survive a replay. Where the state store lives and how it survives is the next part.

State durability: replication and point-in-time recovery

Losing the checkpoint loses no data (the pipeline re-reads) but costs a re-sync. Primary-backup replication keeps the state store and job metadata on one primary that orders writes and ships them to a standby. Because the merge makes replay harmless, an asynchronous acknowledgement rule is enough: a failover that drops a few checkpoints re-reads a few batches. Only the primary acknowledges state writes; a standby never accepts checkpoints on its own.

Point-in-time recovery covers mistakes that replication copies faithfully, such as a bad deploy that wrote wrong cursors: base backups plus the archived change log rebuild state as of any instant. Contract: restore state to a moment at or before the target's restore point; an older checkpoint only causes a replay, a newer one skips rows.

Schema manager and staging format

Sources change shape: columns are added, renamed, widened or dropped, API fields appear. schema-evolution (no page yet) is the policy for each case. Typical rules: a new column is added automatically; a widened type is applied; a dropped column keeps its history and is filled with nulls; an incompatible change pauses the stream and alerts, because guessing destroys data. For file and message formats the rules come from the format (Avro defines how a reader schema resolves against a writer schema). The catalog from Airbyte discover is the input to the diff.

Between extractor and loader, records are batched into files. A columnar-file-format (no page yet), in practice Parquet, makes the batch compact, typed and cheap to bulk-load. Parquet keeps min/max statistics per column chunk and page, so a reader can skip data (parquet-format README). JSON lines remain fine for small streams.

Loader: idempotent merge into the target

At-least-once delivery means the loader sees duplicates, so every write must be idempotent in effect. Two mechanisms combine:

  • Upsert by primary key. upsert-merge (no page yet) loads the batch into a staging table, then runs a single MERGE (or delete-and-insert in one transaction) keyed on the primary key, so replaying a batch rewrites the same rows with the same values. Delta Lake and Iceberg document MERGE INTO for this. Without a primary key the only safe modes are append (keeps duplicates) and full overwrite (dlt: append, replace, merge).
  • Idempotency keys for non-idempotent targets. When the target is an API rather than a table, send a key per logical write so a retry returns the stored result; see Idempotency keys. Stripe documents the pattern: the server stores the first result under the key and replays it. Within the pipeline, batch id plus stream position is the natural key.

Deleted rows need a tombstone: CDC emits a delete event, and the merge turns it into a delete or a soft-delete flag. A pure updated_at poll cannot see deletes; reconcile with a periodic key-only comparison.

Error path

Some rows will never load: a type that does not fit, a value over a size limit, a record the schema manager rejects. Failing the stream on one bad row blocks every row behind it; dropping it loses data. A Dead-letter queue takes the bad record with its error and context, lets the stream continue, and gives an operator a place to inspect, fix and replay. Kafka Connect exposes this as configuration (errors.tolerance, errors.deadletterqueue.topic.name; default fail-fast). Put a threshold on it: a high dead-letter rate signals a schema or mapping error and should stop the stream.

Run supervision, draining and service identity

Process supervision starts extractor and loader processes, reaps their exits and restarts them under a delay and burst limit, then stops them with SIGTERM before SIGKILL; without the limit a connector that dies at start-up loops against a rate-limited source. The supervisor restarts processes; the scheduler retries runs.

Graceful draining is what SIGTERM triggers in a deploy: the connector stops reading pages, the loader commits the batch in flight, the state store persists its position, and the process exits before the kill deadline. A late exit is still safe, since the merge turns the replay into a no-op.

Mutual TLS gives each internal hop (connector to broker, loader to state store) a verified, rotatable machine identity instead of a shared password. Secrets management holds the source credentials; mTLS covers the platform's own connections.

Verification and observability

Coverage-guided fuzzing targets code that parses bytes the platform does not control: source payloads, CDC messages, staging files and catalogs. A feedback-driven mutator finds the malformed record that crashes a connector before a source does.

Deterministic simulation testing runs source, connector, loader and state store in one thread under a seeded scheduler with simulated network, disk and clock. It injects a crash between the loader's commit and the state write at every point, checking "no row missing, none duplicated after merge"; a failing seed replays exactly.

hdr-histogram (no page yet) records per-stream latency and freshness (source commit to target commit) with bounded relative error. Workers record locally and the platform adds the counts, so percentiles survive aggregation.

Data and control flow

One incremental sync of a database table (CDC):

  1. The scheduler starts the run; the platform loads the stream's saved state (a log position).
  2. The connector opens a replication stream from that position and reads change events (insert, update, delete), ordered per table.
  3. Events are batched by size or time (order of seconds to minutes) into staging files with the last log position.
  4. The loader reads a staging file into a staging table and runs a keyed merge into the target in one transaction; deletes become deletes or flags. Rows that fail go to the dead-letter queue.
  5. After the commit, the platform persists the batch's log position as state, and the connector acknowledges it to the source so the slot can release WAL.
  6. A crash between steps 4 and 5 replays the batch; the merge makes the replay a no-op.

One first load plus backfill:

  1. Record the current log position, then start the change stream so no change is lost.
  2. Split the table by primary key range (Chunked backfill); copy chunks in parallel within the source's limits; each finished chunk is checkpointed.
  3. Merge chunk rows and change events by primary key; the newer log position or version wins.
  4. When all chunks are done and the stream has caught up, mark the table live.

Scaling

Pull the levers in this order:

  1. The source's limits are the first ceiling: API rate limits and connection counts. Levers: multiple credentials where the terms allow, only the fields you need, incremental instead of full syncs, log-based CDC.
  2. Parallelism across streams and chunks. Both are independent; run them as separate tasks, bounded by the token bucket and source connections.
  3. Batch size and file format. Larger batches amortise per-commit cost in the warehouse; columnar staging files cut bytes moved. Airbyte reports 4-10x gains from replacing JSON through the orchestrator with a socket data channel (Airbyte protocol docs): a process in the middle can be the bottleneck, not the network.
  4. The loader. Merge cost grows with target size unless it can prune by partition or cluster key; partition on the incoming key or date, or switch hot streams to append plus a deduplicating view.
  5. A broker between extractor and loader (Kafka Connect, for example) absorbs bursts and lets several loaders read one stream, at the price of another system to run.

Failure modes

FailureSymptomCauseFix
Duplicate rows in the targetthe same id twiceat-least-once replay with append-only loadmerge by primary key; idempotency key on API targets
Missing rows after a crashgaps in the targetstate committed before the loader finishedcommit state after the loader's transaction; simulate the crash between the two
Missed updates at a cursor boundarysame-timestamp rows never arrivestrict > on a non-unique cursorcomposite cursor or re-read an overlap
Silent missed deletesdeleted source rows persist in the targetpolling cannot see deleteslog-based CDC, or periodic key reconciliation
Source disk fillsWAL growingreplication slot held by a stopped consumeralert on slot lag, set max_slot_wal_keep_size
Retry storm429s growworkers retry in step, no jittertoken bucket per credential, jittered backoff
Schema drift breaks loadsload errors after a source releasecolumn changed upstreampolicy per change type; pause on incompatible change
Poison row blocks a streamstream stuck on one batcha row the target rejectsdead-letter queue with a rate threshold
Backfill and live changes disagreeold value overwrites a newer oneno ordering between snapshot and change eventsstart the change stream first; newer position wins

Real implementations

namewhat it isrepolicencenotes
Airbyteconnector platform; spec/check/discover/read protocol, state messagesairbytehq/airbyteElastic License 2.0 (root LICENSE file)sync modes: full refresh, incremental append, deduped, CDC
Debeziumlog-based CDC connectors for MySQL, PostgreSQL and moredebezium/debeziumApache-2.0runs on Kafka Connect, standalone or embedded
Kafka Connectruntime for source and sink connectors, offsets topic, dead-letter topicsapache/kafkaApache-2.0docs: user guide
dltPython library: pipelines with state, schema inference and evolution, append/replace/mergedlt-hub/dltApache-2.0embeds in your own code
Meltanoorchestrates Singer taps and targetsmeltano/meltanoMITuses the Singer message contract
Singerthe tap/target specification (SCHEMA, RECORD, STATE)singer-io/getting-started—the spec is in docs/SPEC.md
Apache NiFiflow-based dataflow engine with backpressure and provenanceapache/nifiApache-2.0generic dataflow tool

The alternative: query the source in place (federation). It avoids the pipeline but loads the operational database and cannot keep deleted history. Choose a pipeline for history, cross-source joins or isolation.

Sources

Similar systems

Share the most building blocks with this one.