Library// diagnostic

The ingest queue backs up every Monday morning

In short

A queue that drains all week and refills every Monday is behaving exactly as designed. The source hands over a week of work in a few hours, the pipeline is sized for the weekly mean, and the backlog is the arithmetic of that mismatch. The durable fix is a scheduling decision — move the burst or reserve capacity for it — not a larger machine.

Key takeaways

  • Plot arrival rate, processing rate and queue depth over 7 days. One chart names the cause.
  • A pipeline running at 5 times the weekly average can still be a full day behind by Tuesday.
  • Adding workers against a rate-limited dependency raises the rejection count, not the throughput.
  • One record in an ordered partition holds up everything behind it while other partitions drain.
  • Queue depth is a lagging alert. Alert on projected drain time and the age of the oldest item.
  • Containment buys hours. Only moving or reserving for the burst changes next Monday.

The queue is empty by Thursday and 30,000 deep by Monday lunchtime, and nothing in the logs is red. That is not a defect. It is the shape of the arrivals meeting a pipeline sized for their average, and until both are plotted on one axis the team keeps hunting a bug that does not exist.

Do the arithmetic first. A weekly export drops 42,000 documents between 23:00 Sunday and 02:00 Monday; the pipeline sustains 1,400 an hour. Arrivals run at ten times the service rate, so the queue peaks near 37,800 and needs about 27 further hours to clear — the last document lands some time on Tuesday morning. Averaged over 168 hours the source sends 250 an hour, so the pipeline holds over five times the capacity it needs and is still a day late.

Three lines across seven days, before you change anything

The confirming observation is one chart covering a full week, ideally two, with time-of-week on the horizontal axis: arrivals per interval, completions per interval, and queue depth. Hour buckets are usually right; anything coarser than 4 hours hides the burst.

  • Arrivals per hour. Group by hour-of-week, not by date, so the recurrence survives. If over 80 percent of the volume lands in 5 percent of the hours, the shape is the story.
  • Completions per hour. The ceiling of this line is the real capacity, whatever the design document claims. A flat top under load is a limit, not a busy machine.
  • Queue depth per stage, not per pipeline. A total says there is a backlog; a per-stage figure says which stage owns it, which is most of the diagnosis.
  • Age of the oldest unprocessed item. Depth can fall while the oldest item gets older — a stalled ordered group, seen from outside.
  • Distinct source identifiers against items handled. If 61,000 items came from 42,000 records, a third of the work was reprocessing.

Five things that turn a Monday into a wall

The chart narrows it to one of five causes, each with a distinguishing observable. Read for the row whose middle column matches what you plotted, not your suspicion.

CauseWhat the plot showsThe confirming checkWhere the fix sits
A weekly export windowArrivals spike into a 2 to 4 hour band; completions stay flat for the next dayGroup arrivals by hour-of-week across 3 weeks. The same band lights up every timeThe source's schedule, or a buffer metering delivery into the pipeline
A retry stormArrivals exceed anything the source could have sent, with an echo one retry interval laterDistinct source identifiers against items handled. A ratio above 1 is reprocessingBackoff with jitter, a receive-count ceiling, a dead-letter destination
One expensive stage with no concurrencyDepth grows in front of a single stage while the stages either side sit idlePer-stage depth and duration. One stage owns almost all the wall-clock timeConcurrency on that stage alone, or a cheaper implementation of it
A rate-limited dependencyCompletions form a flat line at a suspiciously round number, unmoved by extra workersRejected responses against worker count. Rejections rise, throughput does notOne shared token bucket sized to the documented limit, not per-worker concurrency
A poison record in an ordered partitionOne partition climbs while the others drain; the same identifier recurs in the logOldest-item age per partition, and the receive count on the stalled head itemA dead-letter path with a receive-count threshold, so one record cannot block a queue
Five causes of a recurring backlog, what each looks like on the week-long plot, and the confirming check

The last row gets reached for last and should be reached for early: it is the only cause where total throughput looks healthy while a slice of the corpus is stuck. Ordered delivery is the mechanism. Amazon's SQS documents that in a FIFO queue, messages sharing a message group identifier are processed in strict sequence, and while one is in flight the rest of that group stays unavailable until it is deleted or its visibility timeout expires — 30 seconds by default, extendable, never beyond 12 hours from first receipt. A record failing after 25 seconds reappears every half minute indefinitely, and everything behind it waits.

What to do while the backlog is live

Containment means ordering the moves so the cheap, reversible ones happen first. Doubling the workers is the instinct and is third at best, because two of the five causes get worse when you do it.

  1. Stop the reprocessing before adding anything. If the distinct-identifier ratio said retries, capping the receive count and routing failures to a dead-letter queue removes work instead of adding capacity. A pipeline retrying at scale competes with itself for the same limited dependency.
  2. Clear the stalled partition by hand. Pull the head item, record its identifier, push it to the dead-letter path. One released record can take an hour off the drain estimate.
  3. Raise concurrency only on the stage that is queued. Per-stage depth already named it. Raising it everywhere moves the bottleneck downstream and destroys the evidence.
  4. Check the ceiling before adding workers against an external dependency. Where the limit is published in requests or tokens per minute, workers beyond that number become rejections. Size one shared token bucket to the documented figure.
  5. Split the lane rather than shedding load. A cheap metadata pass on everything, with expensive derivation queued behind it, makes new arrivals findable in minutes while the heavy work stays a day behind.
  6. Record the run identifier, the counts and every intervention before the backlog clears. Once the queue is empty the evidence for next Monday is gone — which is why derived records should carry the run and transform identifiers that make an answer traceable.

The pipeline is not undersized. It is sized for a week that never happens — an average nobody experiences, spread evenly across hours the source never uses.

Moving the burst instead of buying capacity to absorb it

Four durable options exist and they trade against each other. The cheapest is usually a conversation with whoever owns the export schedule.

ResponseWhat it doesWhat it costs
Change the source scheduleA weekly export becomes daily or hourly, so arrivals approach the mean the pipeline was built forNothing technical, and often months of asking. Some systems only support a full export
Meter the burst through a bufferAccept everything at once, release into the pipeline at a fixed rateWorst-case age gets worse by design, and that number belongs in a written freshness commitment
Reserve capacity for the peakScale workers on queue depth so the burst clears in hours rather than a dayPaying for a peak used a few hours a week, and the ceiling still sits at the slowest dependency
Two-speed processingA cheap pass makes everything findable at once, expensive derivation follows behindTwo code paths and a window where an item is retrievable but not yet enriched
Durable responses to a recurring arrival spike, and what each one costs you

The middle two rows get argued as if one were obviously right. They are not comparable without a number: the buffer wins where the business tolerates a documented worst-case age, the reserved capacity wins where it cannot. Where the choice is open, cost them out on one page — the discipline in modelling three options for one requirement side by side.

Whether the arrival shape can move depends on how the pipeline learns something changed. A source emitting notifications can be consumed continuously; a source that only produces a scheduled dump cannot, and the burst is then a property of the capture mechanism — the subject of change data capture applied to a document corpus.

Alerting on a queue that is supposed to fill

A depth threshold is the wrong alert, because depth is meant to spike and an alert that fires every Monday is unread by the third week. Two signals carry the information instead.

  • Projected drain time. Depth divided by the completion rate of the last 15 minutes, alerting when the projection crosses the hour the work stops being useful. A depth of 30,000 is routine at 14,000 an hour and an incident at 400.
  • Oldest unprocessed item, per partition. Catches the stalled group that healthy total throughput conceals.
  • Arrival volume against the same hour in prior weeks. A Monday at triple its usual size is a source-side event, often a re-export of history.
  • Rejection rate plotted next to worker count. Rising together is a rate limit, invisible in a success rate that counts the eventual retry.

All four want somewhere to live. An operator view showing per-stage depth, oldest-item age, dead-letter contents and a replay button is unglamorous internal tooling and operations software, and it pays for itself the first time someone answers "is Monday's export in yet" without opening a log. The wider question is the alerts that catch a pipeline lying about its own success.

What a shorter queue will not fix

A drained queue is a lateness fix, not a correctness fix, and the two get confused whenever a backlog resolves and a complaint does not. If a document is still wrong once the queue empties, the queue was never the cause — the failure isolated in an updated document that is still served as its old text.

Two neighbouring problems look like this one and are not. A job running for days is usually not progressing rather than progressing slowly — see the reindex that never finishes. A run that completes fast with a fraction of the records is silent failure, covered in a nightly sync that reports success while records are missing. Both sit in data readiness and pipelines, part of the engineering library.

Frequently asked questions

Short answers to the follow-ups this page tends to raise.

Why does my data pipeline fall behind on the same day every week?

Because the source delivers a week of work in a few hours and the pipeline is sized for the weekly average. Plot arrivals per hour, completions per hour and queue depth across a full week: if most of the volume lands in a narrow band and the completion line is flat, the pipeline is slower than the burst rather than broken. Five times the average capacity still takes over a day to clear arrivals running ten times faster than it can work.

Should I add more workers to clear an ingestion backlog?

Only after confirming the bottleneck is worker capacity rather than a limit or a stall. If the slow stage calls a rate-limited dependency, extra workers become rejected requests and retries, raising total load. If an ordered partition is stalled on a failing record, extra workers do nothing, because the items behind it are not available for delivery. Check per-stage depth and rejection rate first.

What should a queue-depth alert actually fire on?

Projected drain time and the age of the oldest unprocessed item, not raw depth. Depth is expected to spike when a batch lands, so a depth threshold either fires every week or is set so high it never warns in time. Dividing current depth by the completion rate of the last 15 minutes gives a projection you can attach to a business deadline, and per-partition oldest-item age exposes a stalled group that healthy overall throughput hides.

How can total throughput look fine while some records are still not processed?

Because ordering guarantees strand a subset while the rest flows. In queues preserving order within a group or partition, a record that fails mid-processing becomes available again after a timeout and is retried ahead of everything behind it, so that group makes no progress while the others drain. Aggregate throughput barely moves. Per-partition oldest-item age exposes it; a receive-count threshold with a dead-letter destination fixes it.

  • ingestion
  • capacity
  • queues
  • diagnosis
// shipped work

The work behind this page

Builds from our portfolio that this page draws on.

Read next

Working on something in this space?

Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.

Start the conversation