Distributed Ingestion Pipeline

A distributed ingestion pipeline is about safety more than speed. Delivery is at least once, so every write must be harmless to repeat: chunk IDs come from the content, writes are upserts, and old chunks are removed after the new ones land. Otherwise a crash leaves duplicates, gaps or stale text.

Overview

A distributed ingestion pipeline is about safety more than speed.

In this guide
  1. Why the design is about safety, not speed
  2. Size the workload first
  3. Deterministic chunk IDs
  4. Three layers of idempotency
  5. See what a crash does
  6. Recovering from failures
  7. Clean up stale chunks
  8. Detect changes before you queue work
  9. Why Celery rather than asyncio
  10. Monitor it
  11. Things to try with Distributed Ingestion Pipeline
  12. What to remember about Distributed Ingestion Pipeline
  13. Questions people ask about Distributed Ingestion Pipeline
  14. Distributed Ingestion Pipeline: recap in one screen

Parameters

Visualisation

—

Readout

What to watch

  • With random IDs, each redelivery adds a duplicate.
  • At-most-once delivery shows no duplicates and loses chunks.
  • Only the third safety setting leaves a clean index.

Distributed Ingestion Pipeline

Process thousands of articles a day with idempotent workers, so a crash, a retry or a redelivered message never leaves a duplicate or a hole in your index.

Why the design is about safety, not speed

A production RAG system takes in new and updated documents every day. The ingestion pipeline splits each article into chunks, makes embeddings and writes them to the vector store. At 10,000 articles a day, raw throughput is modest. One worker can embed the whole batch in about a minute.

So why distribute it? For safety. If one article has malformed text, it must not block the other 9,999. If the embedding API rate-limits you halfway, finished articles must not be redone. If a worker dies between embedding and writing, the system must recover without leaving duplicates.

Three choices give you those properties. A message queue feeds idempotent workers. Chunk IDs are derived from the content, so they are the same every time. And a dead-letter queue catches messages that can never succeed.

This page covers sizing, the worker design, the three layers of idempotency, failure recovery, change detection and monitoring. The patterns suit any corpus that feeds a vector store, whether you use Celery with Redis or Kafka with your own consumers.

One idea runs through all of it. Assume every step can run twice, or stop halfway. Then make sure the index ends up right either way. That single habit removes most of the failures that haunt ingestion jobs in production.

Size the workload first

Quantify the job before you design anything. The answer tells you whether you need a distributed design, and how many workers. The latency per batch below is an assumption, so measure your own.

example_01.pyNumPy
Output

One worker finishes in well under a minute. The real reasons to distribute are bursts (10,000 articles arriving in one hour, not spread over 24), failure isolation, and room to grow.

Deterministic chunk IDs

Idempotency starts with the chunk ID. If the ID comes from the content, reprocessing the same article produces the same IDs. Writing those chunks again overwrites the old records instead of adding copies.

example_02.pyNumPy
Output

A counter or a random ID breaks this. Each reprocessing run would mint new IDs and create duplicates. Notice what a changed chunk does: it gets a new ID, so the old chunk is left behind. The cleanup section below deals with that.

Three layers of idempotency

Processing the same article twice must give the same result. Three layers make that true.

Layer 1: message deduplication. A FIFO queue or Kafka offsets stop the same message being delivered twice inside a window.

Layer 2: version checking. Before work starts, the worker checks whether this version is already done. This catches redeliveries that got past layer 1.

Layer 3: content-addressed writes. Chunk IDs come from content, and writes are upserts, so the final state is the same however many times a chunk is written.

example_03.pyNumPy
Output

See what a crash does

Delivery is at least once. If a worker dies before it acknowledges a message, the queue sends it again. That is the safe choice, and it means some chunks get written twice. The simulation below crashes a worker after the write and before the acknowledgement, 10% of the time, and compares two ways of writing.

example_04.pyNumPy
Output

With random IDs, every redelivery leaves a copy: the store ends with 1,099 entries for 1,000 chunks. With content IDs, the repeat write lands on the same key and the count stays at 1,000. At-least-once delivery plus an idempotent write is the standard pattern. Exactly-once delivery is either impossible or very costly in a distributed system. The same idea underlies distributed retrieval.

Recovering from failures

Three kinds of failure get three responses.

Transient failures such as timeouts and rate limits trigger a retry with exponential backoff, for example waits of 60, 120 and 240 seconds.

Poison messages, such as malformed content or encoding errors, go to a dead-letter queue after the retries run out. It is a place to investigate, not a graveyard. Each entry needs enough context to diagnose the cause and replay it after a fix.

Partial failures, for example three of five embedding batches succeeding, trigger a retry of the whole article. Because IDs are deterministic and writes are upserts, the finished chunks are overwritten with identical data. No duplicates and nothing stale.

Clean up stale chunks

When an article changes, the new version may produce different chunks. The old chunks that no longer exist must go. The order matters: write the new chunks first, then delete the old ones. If you delete first, there is a window where the article has no chunks and queries on it come back empty.

example_05.pyNumPy
Output

Detect changes before you queue work

Most articles do not change on a given day. In a corpus of 200,000 articles where 10,000 change daily, change detection spares you from reprocessing 190,000 unchanged ones. Hash each article and compare it with the stored hash. Only articles with a new hash enter the queue.

example_06.pyNumPy
Output

Why Celery rather than asyncio

For this workload Celery with Redis or RabbitMQ is the practical choice.

Worker isolation. Each worker is a separate process. If one dies from running out of memory, or from a crash in a native embedding library, the others keep going. Asyncio tasks share one process, so one crash takes everything down.

Built-in retries. Celery ships retry, backoff, rate limiting and dead-letter routing. With asyncio you write that yourself.

Horizontal scaling. Adding a worker is one command, with no code change.

Asyncio wins at very high rates, such as 100,000 tasks a second, or where a queue hop adds too much delay. Kafka fits better when you need ordering or replay from an offset.

Monitor it

A pipeline you cannot see is a pipeline that fails silently. Track the failure rate, queue depth, dead-letter depth and processing time, and alert when they drift.

example_07.pyNumPy
Output

Things to try with Distributed Ingestion Pipeline

  1. Set the Delivery to at most once with a Worker crash rate of 10. No duplicates appear, and chunks are silently lost.
  2. Set the Delivery to at least once and Safety layers to none. Redeliveries leave duplicate chunks.
  3. Move Safety layers to content IDs and upsert. The duplicates vanish, but stale chunks from edited articles remain.
  4. Move Safety layers to IDs, upsert and cleanup. The index is clean.

What to remember about Distributed Ingestion Pipeline

Distribute for safety, not speed. Use at-least-once delivery with idempotent workers. Give chunks deterministic IDs, check versions and upsert. Write new chunks before you delete old ones. Send poison messages to a dead-letter queue, skip unchanged articles, and watch the failure rate.

Questions people ask about Distributed Ingestion Pipeline

Why not process everything in one batch? One bad article aborts the run. Per-article tasks isolate failures.

Why content hashes instead of counters? A counter mints new IDs on every reprocessing and creates duplicates. A hash gives the same ID for the same content.

How do I handle articles deleted at the source? Keep a list of active article IDs, compare it with the vector store on a schedule, and delete chunks for articles that are gone.

When should I use Kafka? When ordering matters, when you need to replay from an offset, or at very high rates. Otherwise Celery is simpler.

How do I test idempotency? Process the same article twice and check the store holds the same chunks with the same IDs, with the count unchanged.

Distributed Ingestion Pipeline: recap in one screen

  • Distribute for failure isolation and bursts, not raw speed.
  • Content-derived chunk IDs plus upserts make repeats harmless.
  • Use at-least-once delivery, never at-most-once, for an index.
  • Write new chunks first, then delete the stale ones.
  • Hash articles to skip unchanged ones, and alert on failure rate.

Recall check

0 of 3

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. Without scrolling back — what is the one-line takeaway from this module?

  2. What does this module say about “Why the design is about safety, not speed”?

  3. What does this module say about “Size the workload first”?

Cheat sheet

Distributed Ingestion Pipeline

Design idempotent ingestion workers with content-based chunk IDs, upserts, stale-chunk cleanup and a dead-letter queue, and see what a crash does to the index.

GEN AI · vizlearn.in/gen_ai/distributed_ingestion_pipeline.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.

Teaching a course with this? Individual teachers can use it freely; for a whole department or programme, see institutional licences.