Almost every production app needs a background job queue: sending email, generating PDFs, calling webhooks, running scheduled tasks. This guide compares the main options by language, features, tooling and how much work they are to run, and explains where each one can run on Domain India.
Pick the queue that matches your language: BullMQ for Node.js, Sidekiq for Ruby, Celery (or RQ) for Python, Laravel Queues with Horizon for PHP, and Temporal when you need durable multi-step workflows. All of them need a long-running worker process, and most need Redis. On Domain India, run workers and Redis on a VPS: long-running processes are stopped on shared hosting, and neither shared hosting nor the App Platform offers Redis.
1. The six choices
| Queue | Language | Backend | Monitoring UI | Best for |
|---|---|---|---|---|
| BullMQ | Node.js | Redis | Bull Board | Node and TypeScript apps |
| Sidekiq | Ruby | Redis | Built in | Rails apps |
| Celery | Python | Redis or RabbitMQ | Flower | Django, Flask, FastAPI |
| Laravel Queues / Horizon | PHP | Redis, database, SQS | Horizon (Redis only) | Laravel apps |
| Temporal | Any (SDKs) | PostgreSQL, MySQL or Cassandra | Temporal Web UI | Durable multi-step workflows |
| AWS SQS + Lambda | Any | AWS-managed | CloudWatch | Serverless apps |
Raw throughput is rarely the deciding factor. On a small server, most of these handle far more jobs than a typical business app produces; your database and the external APIs your jobs call usually become the bottleneck first. Developer experience and operational maturity matter more. If throughput does matter, benchmark your own job on your own hardware.
2. Decision tree
1. What is your primary language?
- Node.js → BullMQ
- Ruby/Rails → Sidekiq, or Solid Queue (the database-backed default in Rails 8)
- Python → Celery (most common) or RQ (simpler)
- PHP → Laravel Queues, with Horizon for monitoring
- Several languages → Temporal or SQS
2. Do you need durable workflows (multi-step, long waits, human approval)?
- Yes → Temporal
- No → a standard queue is enough
3. Where will the workers run?
- Every option here needs a worker process that stays running. That rules out shared hosting, where processes running outside the web server are stopped.
- VPS → any of them, with Redis installed alongside.
- Serverless → SQS + Lambda, Cloudflare Queues, or an HTTP-based queue such as Upstash QStash that calls your endpoint.
3. BullMQ: the Node.js choice
The successor to the bull package, written in TypeScript.
Strengths:
- Good TypeScript types
- Rich features: rate limiting, priorities, backoff, repeatable jobs, flows (parent and child jobs)
- Bull Board for monitoring
Weaknesses:
- Redis only (or a Redis-compatible server)
- Some advanced features (groups) are in the paid BullMQ Pro
Example:
import { Queue, Worker } from 'bullmq';
import IORedis from 'ioredis';
const connection = new IORedis({ maxRetriesPerRequest: null });
const emailQueue = new Queue('email', { connection });
await emailQueue.add('welcome', { userId: 42 }, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
// Worker process (runs separately and stays running)
new Worker('email', async (job) => {
console.log(`Sending welcome to user ${job.data.userId}`);
await sendEmail(job.data.userId);
}, { connection, concurrency: 10 });4. Sidekiq: the Ruby default
Around since 2012 and still the standard for Rails. See our Sidekiq article.
Strengths:
- Very reliable
- Rich ecosystem (sidekiq-cron, sidekiq-unique-jobs)
- Commercial Pro and Enterprise tiers for batches, rate limiting and more
- Good web UI built in
Weaknesses:
- Ruby only
- Some powerful features are in the paid tiers
Rails 8 also ships Solid Queue, which stores jobs in your database instead of Redis. It is a good choice when you don't want to run Redis, though it still needs a worker process.
5. Celery: the Python standard
from celery import Celery
app = Celery('myapp', broker='redis://localhost:6379/0')
@app.task(bind=True, max_retries=3)
def send_email(self, user_id):
try:
do_send(user_id)
except Exception as exc:
raise self.retry(exc=exc, countdown=60)
# Call
send_email.delay(42)Strengths:
- Several brokers (Redis, RabbitMQ, Amazon SQS)
- Beat scheduler built in
- Large ecosystem
- Works with Django, Flask and FastAPI
Weaknesses:
- Many configuration options to understand
- Debugging can be tricky
- Behaviour changes between major versions
Lighter alternative: RQ
from rq import Queue
from redis import Redis
q = Queue(connection=Redis())
q.enqueue(send_email, 42)RQ is simpler when you don't need Celery's features.
6. Laravel Queues and Horizon (PHP)
class SendWelcomeEmail implements ShouldQueue
{
use Queueable;
public function __construct(public User $user) {}
public function handle(): void {
Mail::to($this->user)->send(new WelcomeMail());
}
}
SendWelcomeEmail::dispatch($user)->onQueue('emails');Strengths:
- Built into Laravel; little configuration for new apps
- Horizon dashboard for monitoring and tagging (Redis queues)
- Drivers for Redis, database, SQS and Beanstalkd
Weaknesses:
- PHP only
queue:workmust run as a long-lived process under a supervisor
7. Temporal: workflows, not queues
A different model. You write workflows as code, and Temporal persists their state so they survive crashes and can wait for days.
// Simplified sketch of a workflow; the real SDK uses proxyActivities() and condition()
export async function processOrder(orderId: string) {
await validatePayment(orderId); // each step is retried on its own
await reserveInventory(orderId);
await waitForSignal('payment-confirmed', '30 days'); // pause for an external event
await shipOrder(orderId);
}Strengths:
- Multi-step workflows with pause and resume
- State is persisted automatically
- Workflow history can be replayed for debugging
- SDKs for Go, Java, TypeScript, Python, PHP and .NET
Weaknesses:
- You run a Temporal server and its database (or pay for Temporal Cloud)
- Overkill for simple "send an email" jobs
- A real learning curve
Best for: multi-step business processes such as order fulfilment and onboarding flows.
8. Feature comparison
| Feature | BullMQ | Sidekiq | Celery | Laravel |
|---|---|---|---|---|
| Rate limiting | Yes | Enterprise | Per-task rate_limit | Job middleware |
| Scheduled jobs | Job schedulers | sidekiq-cron or Enterprise | Beat | Scheduler |
| Retries with backoff | Yes | Yes | Yes | Yes |
| Priority queues | Yes | Yes | Yes | Yes |
| Unique jobs | Job ID / deduplication | Enterprise or plugin | Plugin | ShouldBeUnique |
| Batches (callback when N done) | Flows | Pro | Canvas (chords) | Bus batches |
| Monitoring UI | Bull Board | Built in | Flower | Horizon |
| Failed jobs kept | Yes | Yes (dead set) | Result backend | failed_jobs table |
9. Choose by pain point
"I just need to send emails in the background":
- Laravel: built-in queues
- Rails:
deliver_laterwith Solid Queue or Sidekiq - Node: BullMQ
- Django: Celery with Redis
"I need scheduled, cron-like tasks":
- BullMQ:
queue.upsertJobScheduler() - Sidekiq: the
sidekiq-crongem - Celery: Beat
- Laravel:
Schedule::job(...)inroutes/console.php
"I need to orchestrate multi-step business flows": Temporal.
"I'm on serverless (AWS Lambda, Vercel)": SQS + Lambda, or Upstash QStash (an HTTP-based queue).
"I want as little operations work as possible": a managed Redis service with BullMQ, Cloudflare Queues with Workers, or AWS SQS with Lambda.
10. Patterns that apply everywhere
Idempotency
Jobs can run twice (for example, a retry after a timeout). Make them safe to repeat:
def send_welcome(user_id):
# Check if already sent
if db.email_log.exists(user_id=user_id, type='welcome'):
return
do_send()
db.email_log.create(user_id=user_id, type='welcome')Timeouts
Every job needs a maximum run time; hangs and infinite loops happen. BullMQ has no per-job timeout option, so enforce one inside the processor:
// BullMQ: abort the work after 30 seconds
new Worker('email', async (job) => {
const signal = AbortSignal.timeout(30_000);
await sendEmail(job.data.userId, { signal });
}, { connection });# Celery: hard and soft time limits per task
@app.task(time_limit=60, soft_time_limit=50)
def generate_report(report_id):
...Retry with exponential backoff
# Celery
@app.task(bind=True, autoretry_for=(Exception,), retry_backoff=True, max_retries=5)
def process(self, data):
...Dead-letter handling
When a job fails all its retries, keep it somewhere you can inspect (failed set, dead set or a failed_jobs table) instead of letting it vanish, and review it by hand.
11. Monitoring essentials
Whatever queue you pick, watch:
- Queue depth: backlog size over time
- Processing rate: jobs per second
- Failure rate: failed against succeeded
- Age of the oldest waiting job: are you keeping up?
- Dead-letter count: something is wrong
See our observability article for a monitoring stack.
12. Common pitfalls
13. Running job queues on Domain India
On Domain India shared hosting (cPanel, DirectAdmin and Webuzo), long-running processes such as queue workers, BullMQ, Sidekiq, Celery and WebSocket servers are stopped, because processes running outside the web server are reaped. Redis and Memcached are not available on any shared plan. Shared hosting is for request/response apps.
- VPS: the right home for workers and Redis. Domain India VPS plans are self-managed with full root access on KVM, so you install Redis, your workers and a process manager (systemd) yourself. VPS plans start from ₹553 a month, excluding 18% GST (Domain India list price on 30 September 2026). No backups or snapshots are included, so plan your own backups, including Redis persistence if your jobs matter.
- App Platform: deploys web apps from GitHub or with a deploy token (Node.js is detected automatically; other languages need a Dockerfile). It includes PostgreSQL but has no Redis add-on, so Redis-backed queues need Redis somewhere else.
- Shared hosting: cron jobs run at most every 4 minutes, so cron is not a substitute for a real-time queue. If you need something light, do the work inside the request, or push it to an external HTTP-based queue that calls a URL on your site.
Can I run a job queue on Domain India shared hosting?
No. Queue workers such as BullMQ, Sidekiq and Celery are long-running processes, and those are stopped on Domain India shared hosting. Redis is also not available on shared plans. Run workers and Redis on a VPS.
How many workers can I run on a 2 GB VPS?
It depends on the job. CPU-heavy jobs: start with about as many workers as CPU cores. I/O-heavy jobs (API calls, email): 10 to 20 concurrent jobs is a common starting point. Start small, watch CPU, RAM and database connections, then scale.
Do I need a separate queue server?
Not for small apps; Redis can run on the same VPS as your app. For fault isolation at larger scale, move Redis to its own server.
What happens if Redis crashes with jobs in flight?
With AOF persistence enabled, Redis restores its data after a restart; with the common everysec setting you can lose up to about a second of writes. For critical jobs, also keep a database record that you mark complete only after the job succeeds.
What is the difference between a workflow engine and a queue?
A queue runs short, isolated tasks. A workflow engine such as Temporal runs a multi-step process whose state survives crashes and long waits. You can chain queue jobs to imitate a workflow, but workflow engines are built for it.
Does the Domain India App Platform include Redis?
No. The App Platform includes PostgreSQL but has no Redis add-on. For Redis-backed queues, use a VPS.
Ready to run production queues? Set up Redis and your workers on a Domain India VPS, or ask us through a support ticket which option fits your app.
Full root access to run Redis, queue workers and schedulers the way your app needs.
See VPS plans