Your signup email job takes 180 milliseconds. The email arrives 20 minutes after signup. You optimize the mailer, shave 40 milliseconds off its run time, and the customer still waits 20 minutes.
The missing number is queue wait time: how long the job sat between becoming ready to run and a worker starting it. A fast job can deliver a slow experience when the queue ahead of it is full.
Measure the whole delay
For a job the user expects soon, think in three parts:
- Time until ready. A job scheduled for later should not count as late while it is waiting for its scheduled time.
- Queue wait time. Once ready, how long does it wait for a worker?
- Execution time. Once a worker starts it, how long does
performtake?
A job scheduled for 10:00, picked up at 10:17, and finished at 10:17:00.180 has roughly 17 minutes of queue wait and 180 milliseconds of execution. Looking only at execution time points you toward the wrong fix.
Start with a user-visible example: the account that waited, the job class, its queue, when it was enqueued or scheduled, when it started, and when it finished. Then compare those times with the queue's backlog and worker activity over the same window. An average across every job class can hide a delayed critical queue.
Read queue depth and latency together
Queue depth tells you how many jobs are waiting. Queue latency tells you how long a job has waited. You need both, because 1,000 tiny jobs may clear quickly while ten expensive jobs can hold up a queue for minutes.
In Sidekiq, Sidekiq::Queue.new("default").size reports the number of jobs waiting in that queue, and .latency reports the age of its oldest enqueued job. Sidekiq's monitoring guide recommends latency as the more useful alert when queue depth produces false positives. The oldest-job number is a useful alarm; it is not the wait time of every job. Track individual job waits if you need a distribution.
With Solid Queue, first confirm that the relevant workers are running and listening to the queue. Its worker configuration controls which queues each worker polls and in what order. A queue can contain ready jobs while a healthy worker process is busy with another queue—or while no worker is assigned to it at all.
What the shape of the backlog tells you
Depth rises, then falls after a burst. Enqueue rate briefly exceeded processing capacity. This may be fine for exports or nightly maintenance. For password resets or signup emails, decide how much wait the user can tolerate, then give those jobs capacity of their own.
Depth and wait time keep rising. The queue is receiving work faster than workers can finish it. Check job throughput, worker concurrency, CPU, memory, and database connections before simply adding more workers. More workers can make a saturated database slower and deepen the backlog.
Depth stays low, but one job waits a long time. Inspect that job's queue, scheduling time, retries, and any concurrency limit. A small global backlog does not prove that every queue is healthy.
Jobs start promptly but finish late. This is an execution problem. Open a slow execution and look for repeated SQL, external HTTP calls, large batches, or lock contention. Changing queue priorities will not shorten perform.
Fix the cause you measured
If a slow bulk job blocks urgent work, move the bulk work to a separate queue with its own worker capacity. In Solid Queue, queue order matters: workers configured with multiple queues poll them in the listed order, so a lower-priority queue may wait behind a busy higher-priority one. In Sidekiq, inspect the queues each process actually consumes and how its concurrency is allocated.
If the queue is consistently overloaded, reduce unnecessary enqueues or increase processing capacity after checking the shared dependencies. If a single job runs for minutes, split it into bounded batches or remove the expensive work inside it. If scheduled jobs become ready late, inspect the scheduler or dispatcher path before tuning workers.
Make one change at a time. Compare the same queue's wait time, depth, and completed jobs before and after the change. The goal is not an empty queue at every instant; it is a wait time that fits the promise you made to users.
A small monitoring checklist
For each important queue, keep these signals together:
- Oldest ready job age or queue latency.
- Waiting job count.
- Jobs completed and failed over time.
- Worker availability and busy capacity.
- Execution time by job class.
Alert on time waited, with a threshold based on the job's purpose. A digest email may tolerate minutes. A password reset usually cannot. Review the alert after a real traffic burst: did it point to a queue that affected users, and did you have enough context to see why?
DeadBro captures supported background job executions with their SQL and error context, and its system monitor can show Sidekiq queue size and latency. Put those alongside the time the user expected the work to finish. When the job trace is fast and the queue wait is high, you know where to investigate next.
The next time someone says "background jobs are slow," ask which clock they mean. The answer decides whether to open the job's code or the queue in front of it.