DEV Community

Abhinav Pasham
Abhinav Pasham

Posted on

asyncio.Queue

Why Does Python Need asyncio.Queue?

INTRODUCTION

After understanding asyncio.Semaphore and asyncio.Lock, I started wondering how real backend systems handle thousands of incoming requests.

A semaphore limits how many coroutines can execute simultaneously.

A lock protects shared data.

But another question came to my mind.

If all the workers are busy, where do the remaining tasks wait?

Imagine thousands of users uploading images or placing orders simultaneously. The workers can't process everything instantly, so there has to be a mechanism that stores the incoming work until a worker becomes available.

That's exactly the problem asyncio.Queue solves.

In this article, I'll explain why Python introduced asyncio.Queue, the problem it solves, and how it is used in real-world asynchronous applications.


What You Will Learn

  • Why asyncio.Queue exists
  • The Producer-Consumer problem
  • How Queue works internally
  • FIFO (First In First Out)
  • Practical implementation
  • Real-world backend examples

Prerequisites

Before learning asyncio.Queue, you should understand:

  • Coroutines
  • Event Loop
  • asyncio.Semaphore
  • asyncio.Lock

The Problem

Suppose you're building an image processing service.

Whenever a user uploads an image, it needs to

  • Compress the image
  • Generate a thumbnail
  • Store it in cloud storage

Initially I thought every upload could be processed immediately.

await process_image(image)
Enter fullscreen mode Exit fullscreen mode

Now imagine 500 users uploading images at the same time.

Image 1

Image 2

Image 3

...

Image 500
Enter fullscreen mode Exit fullscreen mode

The workers can only process a few images simultaneously.

So another question came to my mind.

If every worker is already busy, what happens to the remaining images?

Should we reject them?

Should they disappear?

Obviously not.

They need a place to wait.

That's where Queue comes in.


What is asyncio.Queue?

asyncio.Queue temporarily stores tasks until a worker becomes available.

Think of it as a waiting room.

Instead of immediately processing every task,

new tasks are placed inside the queue.

Whenever a worker finishes its current task,

it picks the next task from the queue.


How Python Achieves This

Imagine three workers.

Worker 1

Worker 2

Worker 3
Enter fullscreen mode Exit fullscreen mode

Ten jobs arrive.

Job1

Job2

Job3

...

Job10
Enter fullscreen mode Exit fullscreen mode

Initially

Worker1 ← Job1

Worker2 ← Job2

Worker3 ← Job3
Enter fullscreen mode Exit fullscreen mode

The remaining jobs don't disappear.

Instead,

Queue

↓

Job4

Job5

Job6

Job7

Job8

Job9

Job10
Enter fullscreen mode Exit fullscreen mode

When Worker2 finishes,

Worker2

↓

Gets Job4

↓

Queue becomes

Job5

Job6

Job7
...
Enter fullscreen mode Exit fullscreen mode

The queue automatically provides the next available job.


FIFO (First In First Out)

asyncio.Queue follows FIFO.

This means

First Job Entered

↓

First Job Processed
Enter fullscreen mode Exit fullscreen mode

Example

Queue

↓

Job1

Job2

Job3
Enter fullscreen mode Exit fullscreen mode

Worker executes

job = await queue.get()
Enter fullscreen mode Exit fullscreen mode

Now

Worker receives

↓

Job1
Enter fullscreen mode Exit fullscreen mode

Queue becomes

Job2

Job3
Enter fullscreen mode Exit fullscreen mode

Practical Example

import asyncio

queue = asyncio.Queue()

async def producer():

    for i in range(1,6):

        print(f"Added Job {i}")

        await queue.put(i)

async def consumer():

    while True:

        job = await queue.get()

        print(f"Processing Job {job}")

        await asyncio.sleep(2)

        queue.task_done()

async def main():

    producer_task = asyncio.create_task(producer())

    consumer_task = asyncio.create_task(consumer())

    await producer_task

    await queue.join()

    consumer_task.cancel()

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Output

Added Job 1

Added Job 2

Added Job 3

Processing Job 1

Processing Job 2

Processing Job 3
Enter fullscreen mode Exit fullscreen mode

Notice that the producer keeps adding work,

while the consumer processes one job at a time.


queue.task_done()

Initially I didn't understand why we call

queue.task_done()
Enter fullscreen mode Exit fullscreen mode

The job has already been removed from the queue.

So why call another method?

The answer is that removing a job from the queue doesn't mean the work has finished.

It only means the worker has accepted the job.

Only after processing completes,

the worker calls

queue.task_done()
Enter fullscreen mode Exit fullscreen mode

to inform the queue that the job has been completed.


queue.join()

Suppose you want the program to wait until every job has finished.

Instead of manually checking every worker,

Python provides

await queue.join()
Enter fullscreen mode Exit fullscreen mode

It waits until every job that was added using

queue.put()
Enter fullscreen mode Exit fullscreen mode

has been marked as completed using

queue.task_done()
Enter fullscreen mode Exit fullscreen mode

What if Queue Didn't Exist?

Suppose workers are busy.

New jobs keep arriving.

Without a queue,

New Job

↓

??

Enter fullscreen mode Exit fullscreen mode

Where should the job go?

The producer has to wait until a worker becomes free.

Or even worse,

the application may reject incoming work.

A queue acts as a temporary buffer between producers and consumers.


Real-world Use Cases

Image Processing

Upload Image

↓

Queue

↓

Compress Image

↓

Generate Thumbnail

↓

Store in Cloud
Enter fullscreen mode Exit fullscreen mode

The user receives a response immediately,

while the background worker processes the image.


Email Service

When thousands of users register,

emails aren't sent immediately.

Instead,

Register User

↓

Queue Email

↓

Background Worker

↓

Send Email
Enter fullscreen mode Exit fullscreen mode

Order Processing

Customer Places Order

↓

Queue

↓

Worker

↓

Generate Invoice

↓

Update Inventory
Enter fullscreen mode Exit fullscreen mode

Log Processing

Servers continuously generate logs.

Instead of processing every log instantly,

logs are pushed into a queue,

and background workers process them.


Advantages of Queue

  • Stores work temporarily
  • Decouples producers and consumers
  • Prevents losing incoming tasks
  • Makes applications scalable
  • Smoothly handles traffic spikes

Conclusion

Initially, I thought workers would process every task immediately.

But real applications receive requests much faster than workers can process them.

Instead of rejecting work or making users wait,

Python provides asyncio.Queue, which temporarily stores incoming tasks until workers become available.

Once I understood that Queue acts like a waiting room between producers and consumers, the entire Producer-Consumer pattern became much easier to understand.

In the next article, we'll learn how Python automatically acquires and releases asynchronous resources using Async Context Managers (async with).

Top comments (0)