-- online Search!

Async Processing with Queues

by kirupa   | filed under AI Fundamentals

When you drop off clothes at a dry cleaner, you don't stand at the counter while they clean your shirt. They hand you a ticket, you go on with your day, and you come back later with the ticket to pick it up. If ten people show up at once, nobody's turned away. The tickets just pile up a bit, and the cleaners work through them.

Some AI work is like dry cleaning. It takes long enough that making someone stand at the counter (or making a web request hold its connection open) is a bad idea. In the following sections, we'll take one slow AI task, watch it fail as an ordinary web request, and then rebuild it with a ticket and a queue.

Onwards!

A Slow Request

Let's say we're building Minutely, an app that takes the transcript of a 90-minute meeting and turns it into a summary with action items. A 90-minute transcript is long, so our code splits it into chunks, asks a model to summarize each chunk, and then asks the model to combine those summaries into one. All in, that's a bunch of model calls, and the whole job takes a couple of minutes. (Like every timing in this article, that's an illustrative number.)

Here is Minutely, getting a fresh transcript to summarize:

Minutely sits in a chat panel in the corner of a meetings app. The panel header shows a robot avatar with a memo badge, the name Minutely, and the tagline Meeting summaries. A user uploads a transcript file named Weekly sync, 90 min.txt. Minutely replies, Got it! Summarizing your meeting and pulling out the action items. A handwritten note says This is Minutely!

The first version does all of this inside a single web request:

The browser uploads a transcript and waits on one request while our server sends chunk 3 to the model. The browser shows a 504 timeout because a proxy gave up after 60 seconds, while the server is still working.

This breaks in a few frustrating ways. Load balancers and proxies often close connections that have been quiet for a while, so the user can get a timeout error while our server is still happily working. The user, seeing an error, clicks Try again, and now we're summarizing the same meeting twice and paying for it twice. If our server restarts mid-job, the work is simply gone. And if 50 people upload transcripts at 5:01 pm, right after their meetings end, we fire all of those model calls at once and slam into our provider's rate limits.

The root of the problem is that we tied a slow job to a fast thing, the web request.

Hand Out a Ticket Instead

Let's borrow the dry cleaner's idea. When the transcript arrives, our server doesn't do the work. It writes down a job, puts it in a queue, and immediately answers with a ticket:

The browser uploads the transcript to the API, which adds a job to the queue and immediately sends back a job ID. A worker takes the next job from the queue, calls the model, and saves the summary to Results. The browser checks Results for the status of its job.

The response comes back in milliseconds with the HTTP status 202 Accepted, which literally means I got your request and I'll work on it, but it isn't done. The body includes a job ID, like job_7f3.

Separately, one or more workers are watching the queue. A worker takes the next job, does all the chunking and model calls, and saves the finished summary somewhere, like a database, along with the job's status.

Meanwhile, the browser checks in. The simplest way is polling: every few seconds, it asks GET /jobs/job_7f3 and gets back a status. When the status says done, the summary is right there. Instead of polling, we could also push a notification over a WebSocket, send an email, or call a webhook if the caller is another server.

What a Job Goes Through

Every job moves through a small set of states, and it's worth showing each one to the user honestly:

StateWhat the user sees
queuedWaiting in line...
runningSummarizing chunk 3 of 6
doneYour notes are ready
failedSomething went wrong. Retry?

A line like Summarizing chunk 3 of 6 is a nice touch. The user can close the tab, come back, and see real progress instead of a mystery.

Why the Queue Helps

Now let's go back to the problems from before. The timeout problem is gone, because no request stays open long. The upload and each status check finish in a blink.

The 5:01 pm rush gets absorbed too. The queue soaks up the burst, and our workers pull jobs at a pace our provider's rate limits can handle:

Jobs arriving per minute stay low, then spike right after 5:00 pm as meetings end. The workers process jobs at a steady, higher rate until the backlog drains a few minutes later, and then drop back down.

Some users wait a bit longer at peak time, but nobody gets an error, and we never flood the provider. If the line keeps growing for hours, that's a sign we need more workers or need to start turning work away, which is a topic called backpressure and load shedding.

Crashes stop losing work as well. Most queues don't delete a job when a worker takes it. The job is hidden for a while, and the worker deletes it only after finishing. If the worker dies halfway through, the job reappears, and another worker picks it up.

The Catches

That last property comes with a catch. Since a job can reappear, the same job can run twice. A worker might finish the work and then crash right before it deletes the job. Most queues promise at-least-once delivery for this reason: every job runs, but some might run more than once.

So our workers need to be idempotent, which is a fancy way of saying running the same job twice has the same effect as running it once. For Minutely, the worker can check whether job_7f3 already has a saved summary before doing anything. That check matters even more if a job does something with side effects, like emailing the summary to everyone in the meeting.

Here is that double run, and the check that makes it harmless:

Four steps. A worker takes job_7f3, which is hidden rather than deleted. The worker finishes but crashes right before it deletes the job. job_7f3 reappears, and another worker picks it up. That worker first checks whether job_7f3 already has a saved summary. It does, so the worker skips it, and running the job twice has the same effect as running it once.

Some jobs fail no matter how many times we retry them. Maybe a transcript is corrupted, or it's so long that it breaks our chunking. We don't want that job bouncing around the queue forever, so after a few attempts (with a growing delay between each one), we move it to a separate dead-letter queue:

A worker takes a job from the queue and saves successful results. A job that has failed three times moves down to a separate dead-letter queue, where a person looks at it.

The dead-letter queue is where a person goes to see what broke. It's also a great thing to alert on. A sudden pile of dead letters usually means we shipped a bug.

When to Reach for a Queue

Queues add moving parts: the queue itself, the workers, a place to store job status, and the code for checking in. For a chatbot answer that takes three seconds and is being streamed to the user, that's overkill.

A queue earns its keep when a task takes longer than a request comfortably can, when it makes many model calls, when traffic arrives in bursts, or when nobody's actively watching the result appear. Long document processing, transcription, report generation, and agent runs that take minutes are all good fits. If the work can wait hours instead of minutes, and there's a lot of it, a provider's batch API (a way to hand over a big pile of requests at once and collect the answers later, usually at a discount) might be an even better fit. We compare the two in Batch vs. Real-Time Inference.

Conclusion

Minutely's first version tied a two-minute job to a single web request, and it broke in all the ways that combination breaks: timeouts, double work, lost jobs, and rate-limit pileups. Handing out a ticket fixed most of that. The API accepts the job and returns 202 Accepted with an ID, workers pull jobs from a queue at a steady pace, and the browser checks back for the result.

The two things to get right are making workers safe to run twice, since queues can deliver a job more than once, and giving jobs that keep failing somewhere to go besides back into the queue. With those in place, a meeting that ends at 5:00 pm gets its notes a few minutes later, even if everybody else's meeting ended at 5:00 pm too.

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.

Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.

Your support keeps this site going! 😇

Kirupa's signature!

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2026 //--