Handling a Million Tasks in Django: Efficient Prioritization with Celery, Redis, and Flower

Aravind Srinivas
AWS Tip
Published in
5 min readApr 23, 2023

--

In today’s web applications, processing large volumes of tasks simultaneously is common. In one of our recent projects, we encountered a situation where we were receiving more than a million tasks per day. Despite having a powerful machine, we faced delays in processing critical tasks like signup emails, as the workers were occupied with processing other non-important tasks with higher priority. In this blog post, we will demonstrate how we efficiently managed to handle and prioritize these tasks using Django, Celery, Redis, and Flower, ensuring that critical tasks like signup emails are processed promptly.

The Challenge: Prioritizing Critical Tasks Amidst a Million Daily Tasks

In our project, we had a wide variety of tasks, ranging from high-priority tasks like sending signup confirmation emails to lower-priority tasks such as data cleanup and report generation. The main challenge was to ensure that high-priority tasks were executed immediately, while still managing to process the lower-priority tasks efficiently.

Task Management and Prioritization

To address this challenge, we leveraged the prioritization feature provided by Celery. We assigned different priorities to our tasks, with lower values indicating higher priority:

from celery import shared_task

@shared_task(priority=0)
def high_priority_task():
# Your high-priority task logic here
pass

@shared_task(priority=5)
def medium_priority_task():
# Your medium-priority task logic here
pass

@shared_task(priority=10)
def low_priority_task():
# Your low-priority task logic here
pass

Task Routing and Queue Management

We created separate queues for different priorities to efficiently handle and prioritize tasks. This allowed us to allocate dedicated workers to critical tasks, ensuring they were executed promptly. We updated our Celery configuration to include the following task queues:

CELERY_TASK_QUEUES = {
'high_priority': {
'exchange': 'high_priority',
'exchange_type': 'direct',
'binding_key': 'high_priority'
},
'medium_priority': {
'exchange': 'medium_priority',
'exchange_type': 'direct',
'binding_key': 'medium_priority'
},
'low_priority': {
'exchange': 'low_priority',
'exchange_type': 'direct',
'binding_key': 'low_priority'
}
}

We then updated the tasks to route them to their corresponding priority queues:

from celery import shared_task

@shared_task(priority=0, queue='high_priority')
def high_priority_task():
# Your high-priority task logic here
pass

@shared_task(priority=5, queue='medium_priority')
def medium_priority_task():
# Your medium-priority task logic here
pass

@shared_task(priority=10, queue='low_priority')
def low_priority_task():
# Your low-priority task logic here
pass

Monitoring and Scaling Using Flower

To monitor and manage our Celery workers, we used Flower, a web-based tool that provides real-time insights into task progress and worker status. We scaled our workers based on the workload by running multiple worker instances with different concurrency levels, and listening to specific queues:

celery -A myproject worker --concurrency=4 -Q high_priority -n worker1
celery -A myproject worker --concurrency=2 -Q medium_priority -n worker2
celery -A myproject worker --concurrency=1 -Q low_priority -n worker3

This allowed us to allocate more resources to critical tasks, ensuring they were processed promptly, while still maintaining the processing of lower-priority tasks.

Choosing the Right Worker Type and Setting Concurrency

Celery supports different types of worker pool implementations. The two most common worker types are ‘prefork’ and ‘threads’. The choice of worker type depends on the nature of the tasks being executed and the desired concurrency level.

  1. Prefork (default worker type)

The prefork worker type is the default worker pool for Celery. It uses multiprocessing to spawn a separate process for each worker, allowing tasks to run concurrently in separate processes. Prefork workers are suitable for tasks that are CPU-bound or require high levels of isolation between tasks. Each worker process runs independently, so a crash in one worker will not affect the others.

To use prefork workers, simply start the Celery worker without specifying a specific worker pool:

celery -A myproject worker --concurrency=4 -Q high_priority -n worker1

2. Threads (solo or gevent)

Thread-based workers use threading or greenlets (lightweight cooperative threads) to run tasks concurrently within a single process. Threaded workers are suitable for tasks that are I/O-bound or involve network communication, as they can efficiently handle multiple tasks waiting for I/O operations to complete.

To use threaded workers, you need to install the corresponding package, such as ‘billiard’ for solo (thread-based) workers or ‘gevent’ for gevent workers:

pip install billiard
pip install gevent

Then, start the Celery worker with the desired worker pool:

# For solo (thread-based) workers
celery -A myproject worker --concurrency=4 -Q high_priority -n worker1 -P solo

# For gevent workers
celery -A myproject worker --concurrency=4 -Q high_priority -n worker1 -P gevent

Setting Concurrency

The optimal concurrency level for your workers depends on the nature of your tasks and the hardware resources available. In general, you should set the concurrency level based on the number of available CPU cores for CPU-bound tasks, and higher values for I/O-bound tasks to maximize resource utilization.

For example, if you have 8 CPU cores and your tasks are primarily CPU-bound, you might set the concurrency to 8. If your tasks are I/O-bound, you could set the concurrency higher, such as 16 or 32, to keep the CPU cores busy while waiting for I/O operations to complete.

Remember to monitor the resource usage of your workers and adjust the concurrency level as needed to ensure optimal performance.

We faced challenges with memory leaks while using Redis as our message broker and in our Celery workers. We have now discussed how we tackled these issues in a separate blog post, where you can find detailed insights into our solutions.

Best Practices for Managing a Million Celery Tasks

Here are some best practices we followed to ensure efficient handling and prioritization of tasks:

  1. Use task priorities and separate queues for efficient task execution.
  2. Choose the right worker type (prefork, solo, or gevent) based on the nature of your tasks.
  3. Set concurrency levels appropriately for your workers, considering available hardware resources and the nature of your tasks.
  4. Ensure that your Redis server is properly configured to handle the required throughput.
  5. Monitor your Celery workers using Flower and scale them as needed.
  6. Use retries and timeouts for tasks that may fail or take longer than expected.
  7. Regularly monitor and optimize your task code to reduce execution time and resource usage.
  8. Implement rate limiting to avoid overwhelming your system with too many tasks at once.
  9. Regularly backup your Redis data to ensure that no tasks are lost in case of a system failure.
  10. Stay tuned for our future blog post on handling memory leak challenges with Redis and Celery.

By following these best practices, we managed to prioritize critical tasks like signup emails while still processing lower-priority tasks efficiently.

Conclusion

In this blog post, we explored how to address the challenge of handling and prioritizing over a million tasks daily in our Django project using Celery, Redis, and Flower. We demonstrated the importance of task management, prioritization, routing, queue management, monitoring, choosing the right worker type, and setting concurrency levels to ensure that critical tasks are executed promptly and resources are utilized efficiently.

By implementing these strategies and best practices, you can efficiently handle and prioritize a large number of tasks in your web application, improving performance and scalability. Understanding the nature of your tasks and the available hardware resources enables you to make informed decisions regarding worker types and concurrency settings, leading to an optimized application infrastructure.

Need help with software architecture or building highly scalable web applications using Django and React? Don’t hesitate to contact me at srinivasaravind5@gmail.com or connect with me on LinkedIn. I’m always eager to assist fellow developers and entrepreneurs in crafting exceptional products!

--

--

Software Engineering Leader with 7+ years of team management and operational excellence, skilled in building highly scalable products with an agile approach.