Background Tasks Setup: Sidekiq, Celery, Bull
Background tasks are moved out of HTTP cycle: user doesn't wait for operation to complete. Tool choice depends on stack: Sidekiq — Ruby/Rails, Celery — Python/Django/FastAPI, Bull/BullMQ — Node.js.
Sidekiq (Ruby/Rails)
Sidekiq uses Redis as queue storage, supports retries, dead jobs, scheduler.
# Gemfile
gem 'sidekiq', '~> 7.0'
gem 'sidekiq-scheduler'
# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 5]
- [default, 3]
- [mailers, 2]
- [low, 1]
# app/workers/email_worker.rb
class EmailWorker
include Sidekiq::Job
sidekiq_options queue: :mailers, retry: 3, backtrace: true
def perform(user_id, template, variables = {})
user = User.find(user_id)
UserMailer.send(template, user, variables).deliver_now
end
end
# Usage
EmailWorker.perform_async(user.id, :welcome)
EmailWorker.perform_in(5.minutes, user.id, :follow_up)
EmailWorker.perform_at(Time.zone.parse('2024-01-01 09:00'), user.id, :new_year)
Celery (Python/Django)
# celery.py
from celery import Celery
from celery.schedules import crontab
app = Celery('myapp')
app.config_from_object('django.conf:settings', namespace='CELERY')
# settings.py
CELERY_BROKER_URL = 'redis://redis:6379/0'
CELERY_RESULT_BACKEND = 'redis://redis:6379/1'
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_EXPIRES = 3600
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
CELERY_BEAT_SCHEDULE = {
'send-daily-digest': {
'task': 'myapp.tasks.send_daily_digest',
'schedule': crontab(hour=9, minute=0),
},
'cleanup-tokens': {
'task': 'myapp.tasks.cleanup_expired_tokens',
'schedule': crontab(minute=0), # every hour
},
}
# tasks.py
from celery import shared_task
from myapp.models import User
from myapp.services import send_email
@shared_task(
bind=True,
max_retries=3,
default_retry_delay=60, # seconds before retry
queue='emails',
)
def send_welcome_email(self, user_id: int) -> dict:
try:
user = User.objects.get(pk=user_id)
send_email(user.email, 'welcome', {'name': user.first_name})
return {'status': 'sent', 'user_id': user_id}
except Exception as exc:
raise self.retry(exc=exc, countdown=2 ** self.request.retries * 60)
# Usage
send_welcome_email.delay(user.id)
send_welcome_email.apply_async(args=[user.id], countdown=300) # in 5 minutes
# docker-compose: Celery workers
celery-worker:
build: .
command: celery -A myapp worker --loglevel=info --concurrency=4 -Q emails,default
depends_on: [redis, db]
environment: *app_env
celery-beat:
build: .
command: celery -A myapp beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler
depends_on: [redis, db]
celery-flower:
build: .
command: celery -A myapp flower --port=5555 --basic-auth=admin:password
ports:
- "5555:5555"
BullMQ (Node.js)
See separate article. Brief comparison:
| Feature | Sidekiq | Celery | BullMQ |
|---|---|---|---|
| Language | Ruby | Python | Node.js |
| Broker | Redis | Redis/RabbitMQ/SQS | Redis |
| Concurrency | Threads | Processes/Threads/Gevent | Async/Worker threads |
| UI monitoring | Sidekiq Web | Flower | BullBoard |
| Scheduler | sidekiq-scheduler | celery-beat | Built-in repeat |
| Real priority | Yes (queues) | Yes | Yes |
Sidekiq Monitoring
# config/routes.rb
require 'sidekiq/web'
authenticate :user, ->(u) { u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
Flower for Celery is available on port 5555. Shows tasks, workers, delays, retries.
Implementation timeline
Sidekiq or Celery for Django/Rails with basic tasks and monitoring: 2–3 days. With beat scheduler, monitoring and alerting: 3–4 days.







