Ask
128
@fg_stall ·

2,300 bullmq jobs stuck in active after a deploy, lockduration is default Worker Setup

Deployed at 14:02. By 14:05 active was 2,300 and climbing while completed sat perfectly still.

Jobs are image processing, 40-90 seconds each. lockDuration is the default 30000, concurrency 5, managed Redis.

Logs show a job starting, then the same job id starting again on a new pod a minute later, sometimes three times over. Eventually a lot of them landed in failed with a stalled message.

Is 90 second work just wrong for this, or is my deploy wrong?

8 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @r2_bucketeer · 3mo ago · 3 replies

    Both, but mostly the deploy. Two separate problems that look like one.

    The lock. Your job runs three times longer than lockDuration. The lock is renewed while the job is alive and reporting; a job that goes completely silent for 90 seconds looks dead to the stalled checker, which hands it to another worker. Either raise lockDuration comfortably above your p99 job time, or call job.updateProgress() every few seconds inside the work so the lock keeps being renewed. The second is better because it also gives you progress for free.

    The deploy. Your old pods were killed with jobs in hand. Handle SIGTERM: stop accepting new jobs, await worker.close(), and set the container's termination grace period longer than your longest job. The default grace period is 30 seconds and your jobs are 90, so every deploy guarantees a pile of orphans.

    Together those two explain the exact shape you saw - active climbing, completed flat, ids repeating.

    176
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @thermal_theo · 3mo ago

      Rough numbers: lockDuration around twice your p99, stalledInterval under that. And remember every stalled recovery is a full re-run of 90 seconds of image processing, so a bad setting shows up on the compute bill as well as in the logs.

      53
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @overlap_owen · 2mo ago

      We found ours by putting active and completed on one chart. Active rising while completed is flat is the whole signature and it is visible in about five seconds. Worth having that graph before you need it.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @wick_and_wax · 3mo ago

    For 90 second work, chunk it. Split "process image" into download, transform, upload as three jobs of 5-20 seconds each.

    Everything gets better at once: retries become cheap, deploys stop hurting because the window where you can be interrupted is short, progress becomes observable without instrumenting anything, and you can scale the expensive step independently. Long jobs are a design decision, not a requirement.

    88
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @row_level_sam · 3mo ago

    Separately, make the work idempotent so a stalled re-run is boring rather than dangerous. If the transform writes to a deterministic key in object storage, running it three times costs a bit of CPU and nothing else - no duplicate rows, no duplicate emails, no support ticket.

    24
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @kombucha_kai · 3mo ago

    Worth knowing before you tune anything: maxStalledCount defaults to 1, so a job that stalls twice is moved to failed rather than looping forever. That is why some of yours ended up failed instead of running six times - the framework already saved you from an infinite loop. Raise it only when you understand why they stalled.

    44
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @compost_turner · 3mo ago · 2 replies

    Flush Redis and requeue from your own database. Fastest way out of a stuck queue and you know exactly what state you are in afterwards.

    27
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @blown_cap · 3mo ago

      That also destroys the jobs that were perfectly fine, every repeatable and delayed job definition, and anything else sharing that instance. getJobs(['active']), look at them, move the genuinely stuck ones back to waiting. Flushing a queue backend is the kind of thing you do exactly once and then never again.

      18
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report