AI Infra Interviews logo
Coding for Infra / 10
mediumNewAnthropicOpenAI

Write a producer-consumer pipeline with a bounded queue. What are the three bugs that show up in every first attempt?

The queue is four lines and the shutdown is where it goes wrong. One sentinel per consumer rather than one for all of them, an exception path that cannot silently kill a worker, and a bound that must be small enough to actually apply backpressure.

Updated Sep 2026 · Grounded in real AI infrastructure interview loops and written to a senior-engineer editorial bar, with every number worked and every diagram hand-built.

TL;DR: Three bugs appear in almost every first attempt. Putting one sentinel on the queue to stop all consumers, which stops exactly one, because whichever consumer takes it exits and the others block forever: put one sentinel per consumer. Letting an exception escape the consumer loop, which silently reduces the worker count until throughput drops for no visible reason and eventually nothing drains the queue: wrap the work in try/except inside the loop and record the failure. And treating the bound as a memory limit rather than as backpressure, which leads to a queue of 100,000 that never fills and therefore never slows the producer, so a fast producer and a slow consumer accumulate work until the process dies. The bound should be small, a few times the consumer count, so that a full queue blocks the producer promptly and the pipeline runs at the consumer's rate rather than at the producer's.

How to approach it

Write the correct version and then name the three bugs against it, because they are what the question is about. Show the measurement that proves backpressure is working. Then the shutdown ordering, which is the part that is easy to get subtly wrong even after fixing the sentinel count. Close with the choice of threads against asyncio, since the answer depends on what the work does.

A strong answer

A typical situation: a preprocessing pipeline runs with four consumer threads and an unbounded queue. Throughput is fine for an hour, then memory climbs steadily and the process is killed. The producer reads from fast local storage and the consumers do CPU work, so the producer outruns them by a factor of three and every item it produces beyond that accumulates.

The implementation:

import queue, threading

SENTINEL = object()

def run_pipeline(items, work, n_consumers=4, maxsize=8):
    q = queue.Queue(maxsize=maxsize)         # small: this is backpressure, not storage
    results, errors = [], []
    lock = threading.Lock()

    def producer():
        try:
            for item in items:
                q.put(item)                   # blocks when full: the backpressure
        finally:
            for _ in range(n_consumers):      # ONE SENTINEL PER CONSUMER
                q.put(SENTINEL)

    def consumer():
        while True:
            item = q.get()
            try:
                if item is SENTINEL:
                    return
                with lock:
                    results.append(work(item))
            except Exception as exc:          # a failing item must not kill the worker
                with lock:
                    errors.append((item, repr(exc)))
            finally:
                q.task_done()

    p = threading.Thread(target=producer)
    cs = [threading.Thread(target=consumer) for _ in range(n_consumers)]
    p.start()
    for c in cs: c.start()
    p.join()
    for c in cs: c.join()
    return results, errors

Running it with 50 items, 4 consumers, a queue of 5, and one item that raises:

processed 49, errors 1 -> [(13, "ValueError('bad item 13')")]
queue never exceeded 5 items, so the producer was held at the consumers' rate

The three bugs:

bug 1: one sentinel for all consumers
  wrong:  q.put(SENTINEL)                       once, after the loop
  what happens: one consumer takes it and returns; the other three block on q.get() forever;
    the join never completes and the program hangs at exit
  right:  one sentinel per consumer, so each one receives exactly one
  the subtlety: this only manifests with more than one consumer, so a test with a single
    consumer passes

bug 2: the exception escapes the loop
  wrong:  the work call is not wrapped, so an exception propagates out of consumer() and the
    thread dies
  what happens: throughput drops by 1/n with no error visible unless the thread's exception is
    being logged, and after n failures nothing drains the queue and the producer blocks forever
  right:  try/except around the work, inside the while loop, recording the failure
  the subtlety: this looks like a slowdown rather than a crash, which is why it survives

bug 3: the bound is too large to be backpressure
  wrong:  queue.Queue(maxsize=100_000) or queue.Queue() with no bound
  what happens: the queue never fills, so put never blocks, so the producer runs at full speed
    and the difference between producer and consumer rates accumulates in memory
  right:  a small bound, a few times the consumer count, so the queue fills quickly and the
    producer is held
  the arithmetic: with a producer at 3,000 items/s and consumers at 1,000/s, an unbounded
    queue grows by 3,000 - 1,000 = 2,000 items per second
    at 10 KB per item that is 2,000 x 10 KB = 20 MB/s of growth
    against 16 GB of headroom = 16e9 / 20e6 = 800 s, so the process dies after about 13 minutes
    a bound of 8 makes put block, so the pipeline runs at the consumers' 1,000/s and memory
    is flat
sanity: the failure takes 13 minutes to appear, which is long enough to pass every test and
        short enough to happen in production on the first real dataset

Producer-Consumer Pipelines covers the pattern; Batching Queues and Backpressure covers the same idea where the consumer is a remote service.

Shutdown ordering, which stays subtle after the sentinel fix:

the sentinels go in the producer's finally block
  why: if the producer raises partway through, the consumers must still be told to stop, or
  the program hangs on join with a traceback already printed

join the producer before the consumers
  the producer's finally has queued the sentinels by the time it returns, so consumers are
  guaranteed to see them

what about task_done and q.join()
  useful when the producer needs to know all work is complete before signalling shutdown
  the version above does not need it, since the sentinels already order the shutdown, but the
  finally block calls task_done unconditionally so the counter stays consistent if a caller
  does use q.join()

what must never happen
  a consumer returning without calling task_done, which leaves q.join() hanging
  a sentinel consumed by a consumer that then continues, which leaves another consumer without
  one

Threads or asyncio, which decides the shape:

threads
  right when the work releases the interpreter lock: file and network I/O, and numpy or other
  extension code that releases it during computation
  the queue module is thread-safe and blocking, which is what the code above uses

asyncio
  right when the work is I/O awaiting other services, with many concurrent operations
  asyncio.Queue with the same structure, and the sentinel and exception rules are identical

processes
  right when the work is pure Python CPU, since threads there contend on the interpreter lock
  and give no parallelism; the queue becomes a multiprocessing queue and items must be
  picklable, which changes what can be passed

Concurrency in Python, Go and C++ compares the three models.

WHERE A PRODUCER-CONSUMER GOES WRONG bounded queue, N consumers the happy path 4 lines one sentinel per consumer, not one for all shutdown the bug the producer blocks on a full queue, forever, silently a consumer dies the worse bug A hung producer looks exactly like a slow job from outside, which is what makes it expensive. Test the shutdown path first. The happy path never breaks.

The reversal condition: an unbounded queue is correct when the producer is inherently slower than the consumers and the bound would only add lock contention for no benefit, for example a producer reading from a network at 100 items per second feeding consumers that handle thousands. The test is whether the queue ever reaches its bound in practice: if it never does, the bound is doing nothing and could be anything, and if it frequently does, the bound is the mechanism holding the pipeline together. Measuring the queue's high-water mark answers that in one run and is worth instrumenting for exactly that reason.

What interviewers probe next

  • "How do you stop early on the first error?" A shared cancellation flag checked by both sides, plus draining the queue so the producer's put does not block forever on a queue nobody is reading.
  • "What if the producer is also several threads?" A counter of live producers; the last one to finish queues the sentinels. Otherwise the first producer to finish shuts down the consumers.
  • "How would you measure whether backpressure is working?" The queue's high-water mark. Consistently at the bound means the consumers are the limit and the bound is doing its job.
  • "What about ordering?" Multiple consumers destroy input order. If the output must be ordered, tag items with an index and reassemble, or use one consumer.

Common mistakes

  • One sentinel for many consumers, which hangs every consumer but one.
  • An unhandled exception in the consumer loop, which silently reduces the worker count.
  • A bound so large it never fills, which is an unbounded queue with extra steps.
  • Returning from a consumer without calling task_done, which hangs any q.join().

Key takeaways

  • One sentinel per consumer, queued in the producer's finally block so a producer failure still shuts down cleanly.
  • Wrap the work in try/except inside the loop; an escaping exception kills a worker silently and looks like a slowdown.
  • The bound is backpressure: small, a few times the consumer count. At a producer 3x faster than the consumers, an unbounded queue grows 20 MB/s.
  • Instrument the queue's high-water mark; consistently at the bound means backpressure is working.
That one was free — and so are 10 answers per topic without an account. Signing in doubles that to 20, opens the Plus lessons in the courses, and remembers which topics you keep getting wrong.no card · Google sign-in · nothing to cancel
HOW DID IT GO?
0
READING SIGNED OUT

Signing in doubles your free answers, from 10 to 20 per topic, and the site starts remembering you: mastery per topic, bookmarks, and a next-focus recommendation. Free, no card.

Sign in free

The concepts behind this question

Ranked by how closely each one overlaps this question's topic, so the first card is the thing to read if the answer above moved too fast.

Advanced
💻 Coding for Infra🔒 Premium
Batching Queues and BackpressureWrite a request batcher is the coding round's version of the serving engine's scheduler: requests arrive one at a time, the GPU wants them in groups, and the batcher decides when a group is full enough to send without holding anyone too long or accepting more than it can hold. The two knobs are the maximum batch size and the maximum wait, the invariant is a bounded queue, and the follow-ups (priorities, cost-aware batching, cancellation, bounded in-flight batches) are the ideas the real engines carry. This page implements the batcher in asyncio, derives what each knob buys, and walks the follow-ups.
Advanced
💻 Coding for Infra🔒 Premium
Producer-Consumer PipelinesA data loader, a log shipper, a batch inference job and a checkpoint writer are the same program: stages connected by bounded buffers, each running at its own pace, the slowest setting the throughput and the buffers absorbing the jitter between them. The coding screen asks you to build one (read, decode, batch, feed a consumer) and then pushes on the production questions: buffer sizes, clean stops, failure propagation, and why it runs at a third of the expected speed. This page derives throughput from stage times, implements the pipeline in threads and asyncio, and works the stop and failure semantics.
Core
📐 AI Systems DesignSign in
GPU Job Scheduler DesignDesign a scheduler for a shared GPU cluster is the most common design prompt in AI infrastructure interviews, because it touches everything: queues and priorities, gang placement, topology, fairness across teams, preemption and the checkpoints that make it survivable, and the failure handling that keeps a 512-GPU job alive. This page builds the design in layers, states the data model and the scheduling loop, derives the numbers (how long a job waits, how much preemption costs, how much fragmentation wastes), and lists the trade-offs the interviewer will push on.
Foundational
🧮 Napkin Math & Capacity
KV Cache SizingThe KV cache is the memory that decides how many users a serving replica can hold and how long their context can be. Its size per token comes from four numbers in the model's config file (layers, KV heads, head dimension, bytes per element) and one formula; multiplied by context and concurrency it is the number every capacity plan is built on. This page derives it, works it for four models including an MLA one, and shows the two places candidates get it wrong by a factor of eight.
UP NEXT ON YOUR JOURNEY
FEDITOR'S NOTE

Scored on one sentinel per consumer, on the try/finally that keeps a worker alive through an exception, on the bounded queue as backpressure rather than as a memory limit, and on why the bound must be small.

DISCUSSION · 0

No comments yet — be the first to share your approach.