·QueueHub Team·20 min read

Debugging BullMQ Flow Producer Jobs: Parent-Child Chains & Workflow Topologies

BullMQ Flow ProducerBullMQ debuggingjob chainsworkflow debuggingQueue Hub

Single-job workers are straightforward to debug — check the job, check the worker, fix the error, move on. But the moment you model a business process as a multi-step BullMQ flow — with fan-out children, deeply nested chains, and cross-queue dependencies — debugging goes from "check the logs" to "find the needle in a dependency tree."

The most common production incidents with BullMQ flows aren't about Redis connections or worker crashes. They're about dependency topology failures: stuck parent jobs, orphaned children, waiting-children deadlocks, and invisible chain breaks that silently halt entire workflows.

In this guide, you'll learn how to debug BullMQ Flow Producer workflows systematically — from understanding the dependency model to using introspection APIs to leveraging visual tools like Queue Hub for real-time dependency graphs.

What this guide covers:

  • Understanding BullMQ's flow dependency model (parent-child, waiting-children, chain execution)
  • Debugging the 5 most common flow failures
  • Using getDependencies(), getChildrenValues(), and state introspection tools
  • Handling partial failures with ignoreDependencyOnFailure
  • Visualizing flow topology with Queue Hub's job detail and dependency views
  • Dynamic flow patterns (runtime child creation) and their debugging pitfalls
// The simplest flow that can go wrong
import { FlowProducer } from 'bullmq';

const flowProducer = new FlowProducer({ connection });

// Looks harmless — but what happens when "validate-payment" fails?
const flow = await flowProducer.add({
  name: 'complete-order',
  queueName: 'orders',
  data: { orderId: 'ord-123' },
  children: [
    { name: 'validate-payment', queueName: 'payments', data: { orderId: 'ord-123' } },
    { name: 'check-inventory', queueName: 'inventory', data: { orderId: 'ord-123' } },
    { name: 'calculate-shipping', queueName: 'shipping', data: { orderId: 'ord-123' } },
  ],
});

How BullMQ Flows Actually Work (The Mental Model)

Key insight most developers miss: A parent job does not simply "wait" for children. BullMQ implements a dependency barrier — the parent enters the waiting-children state and is not moved to wait until every child has reached a terminal state (completed, failed, or explicitly ignored).

The Lifecycle of a Flow

FlowProducer.add()
  ├── All jobs added atomically (single Redis MULTI/EXEC)
  ├── Children are immediately eligible for workers
  ├── Parent enters "waiting-children" state
  ├── Children process (any queue, any worker)
  │     ├── All children succeed → parent moves to "wait"
  │     ├── Any child fails (without ignoreDependencyOnFailure)
  │     │     └── Parent stays in "waiting-children" forever ← 🔴 MOST COMMON BUG
  │     └── Any child fails (with ignoreDependencyOnFailure)
  │           └── Parent moves to "wait" once remaining children complete
  └── Parent processes → uses getChildrenValues() to aggregate results

State Transitions

    ┌──────────────┐
    │  added       │
    └──────┬───────┘
           │
    ┌──────▼───────┐
    │ waiting-     │◄──── Workers pick up & process children
    │ children     │
    └──────┬───────┘
           │
    ┌──────▼───────┐     ┌───────────────┐
    │  wait        │────►│  active       │
    └──────┬───────┘     └───────┬───────┘
           │                     │
           │              ┌──────▼──────┐
           │              │ completed   │ (parent can access children's results)
           │              └─────────────┘
           │
    (If children fail without ignoreDependencyOnFailure)
    └─────────────────────► STUCK in waiting-children (never reaches wait)

Checking Flow State Programmatically

import { Job } from 'bullmq';

async function diagnoseFlowState(job: Job) {
  const state = await job.getState();
  console.log(`Job ${job.id} state: ${state}`);

  if (state === 'waiting-children') {
    const { processed, unprocessed, failed, ignored } =
      await job.getDependenciesCount();
    console.log('Dependency health:', {
      processed,
      unprocessed,
      failed,
      ignored,
    });

    // Deep dive into which children are problematic
    const deps = await job.getDependencies({
      failed: { count: 100, cursor: 0 },
      unprocessed: { count: 100, cursor: 0 },
    });
    console.log('Failed children:', deps.failed);
    console.log('Unprocessed children:', deps.unprocessed);
  }
}

The 5 Most Common Flow Failures (And How to Debug Them)

1. Parent Stuck in waiting-children Forever

Symptom: A parent job has been in waiting-children for hours or days. The workflow never completes. Downstream processes stall.

Root cause: One or more child jobs failed, and the parent is waiting for ALL children to finish — but failed children are still counted as dependencies.

Debugging with BullMQ APIs:

async function findOrphanFlows(queueName: string) {
  const { FlowProducer, Queue } = require('bullmq');
  const queue = new Queue(queueName, { connection });

  // Get all jobs in waiting-children state
  const stuckParents = await queue.getJobs(['waiting-children']);

  for (const parent of stuckParents) {
    const { failed, unprocessed } =
      await parent.getDependenciesCount({ failed: true, unprocessed: true });

    console.log(`Parent ${parent.id}: ${failed} failed, ${unprocessed} unprocessed`);

    if (failed > 0 && unprocessed === 0) {
      // All children have been attempted, some failed — this is a deadlock
      console.log(`⚠️  Deadlock detected: Parent ${parent.id} has ${failed} failed children`);

      const deps = await parent.getDependencies({
        failed: { count: 100, cursor: 0 },
      });

      for (const [jobKey, jobData] of Object.entries(deps.failed || {})) {
        console.log(`  Failed child: ${jobKey}`, jobData);
      }
    }
  }
}

The fix — ignoreDependencyOnFailure:

// Correct: tell BullMQ this child's failure shouldn't block the parent
const flow = await flowProducer.add({
  name: 'complete-order',
  queueName: 'orders',
  data: { orderId: 'ord-123' },
  children: [
    {
      name: 'validate-payment',
      queueName: 'payments',
      data: { orderId: 'ord-123' },
      opts: { ignoreDependencyOnFailure: true }, // 👈 key addition
    },
    {
      name: 'check-inventory',
      queueName: 'inventory',
      data: { orderId: 'ord-123' },
      opts: { ignoreDependencyOnFailure: true },
    },
    {
      name: 'calculate-shipping',
      queueName: 'shipping',
      data: { orderId: 'ord-123' },
      opts: { ignoreDependencyOnFailure: true },
    },
  ],
});

// Later, in the parent worker, check which children failed:
const childrenValues = await job.getChildrenValues();
const ignoredFailures = await job.getIgnoredChildrenFailures();

2. Deep Chain Breaks (Nested Dependency Collapse)

Symptom: In a deeply nested chain (step-1 → step-2 → step-3 → step-4), the chain stops mid-way. Step 3 never starts, but step 1 and 2 completed successfully.

Root cause in serial chains: In a serial chain (each level has one child), the chain is only as strong as its weakest link. If step 2 fails without ignoreDependencyOnFailure, step 3's parent (which IS the step-2 job) stays in waiting-children forever, blocking the entire chain.

Visual representation:

car (engine)        ← blocked waiting for children
  └─ car (wheels)   ← blocked waiting for children + THIS JOB FAILED
       └─ car (chassis)  ← completed ✓ (but nobody picks up its result)

Debugging — traversing a chain programmatically:

async function traceChain(job: Job) {
  const chain = [];
  let current: Job | null = job;

  while (current) {
    const state = await current.getState();
    const { failed, processed, unprocessed } =
      await current.getDependenciesCount();

    chain.push({
      id: current.id,
      name: current.name,
      queueName: current.queueName,
      state,
      childrenCounts: { failed, processed, unprocessed },
    });

    // Walk up to parent
    if (current.parentKey) {
      const parentQueue = new Queue(
        current.queueName,
        { connection }
      );
      const parentId = current.parentKey.split(':').pop();
      current = await parentQueue.getJob(parentId!);
    } else {
      current = null;
    }
  }

  return chain.reverse(); // root first
}

// Usage:
// const chain = await traceChain(failedJob);
// chain.forEach(j => console.log(`${j.name} [${j.state}] — children: ${JSON.stringify(j.childrenCounts)}`));

3. Missing Child Results (Empty getChildrenValues)

Symptom: The parent job runs successfully but getChildrenValues() returns {} — no child results are available.

Common causes:

  • Children were added to a different queue than expected, and results are stored under a different key namespace
  • Children are still running (parent was picked up prematurely due to a race condition)
  • The child job completed without a return value (returned undefined)

Debugging:

async function debugMissingChildrenValues(job: Job) {
  const childrenValues = await job.getChildrenValues();
  console.log('Children values:', childrenValues);
  // {} — empty

  // Step 1: Check dependency counts
  const counts = await job.getDependenciesCount();
  console.log('Dependency counts:', counts);
  // If processed === 0, children haven't finished yet

  // Step 2: Directly inspect children
  const deps = await job.getDependencies({
    processed: { count: 100, cursor: 0 },
    unprocessed: { count: 100, cursor: 0 },
  });
  console.log('Processed children:', deps.processed);
  console.log('Unprocessed children:', deps.unprocessed);

  // Step 3: Check if children are on the expected queue
  if (deps.processed) {
    for (const [childKey] of Object.entries(deps.processed)) {
      // childKey format: "bull:childQueueName:jobId"
      const [, , childQueue, childId] = childKey.split(':');
      const childQueueInstance = new Queue(childQueue, { connection });
      const childJob = await childQueueInstance.getJob(childId);
      if (childJob) {
        console.log(`Child ${childId} return value:`, childJob.returnvalue);
      }
    }
  }
}

4. Dynamic Child Addition Deadlock

Symptom: A parent job dynamically creates child jobs at runtime (using moveToWaitingChildren and WaitingChildrenError), but then never gets re-evaluated.

The broken pattern:

// ❌ BROKEN PATTERN — parent never re-executes
const worker = new Worker('queue', async (job) => {
  if (job.name === 'parent') {
    // Check if children already exist
    const { processed, unprocessed } = await job.getDependenciesCount();
    if (!processed && !unprocessed) {
      // Add children dynamically
      await queue.add('child', { file: 'report-1.pdf' },
        { parent: { id: job.id, queue: job.queueQualifiedName } });
      await queue.add('child', { file: 'report-2.pdf' },
        { parent: { id: job.id, queue: job.queueQualifiedName } });
    }

    // Tell BullMQ to wait for children
    const shouldWait = await job.moveToWaitingChildren(token);
    if (shouldWait) {
      throw new WaitingChildrenError(); // 👈 parent yields
    }
  }
}, { connection });

// ✅ CORRECT — children must use ignoreDependencyOnFailure
// or the parent will deadlock if even one child fails
await queue.add('child', { file: 'report-1.pdf' }, {
  parent: { id: job.id, queue: job.queueQualifiedName },
  opts: { ignoreDependencyOnFailure: true }, // 👈 critical
});

Debugging checklist for dynamic flows:

  1. Are children being added before moveToWaitingChildren is called? (Race condition if called too early)
  2. Do children have the correct parent.id and parent.queue values?
  3. Is ignoreDependencyOnFailure set if child failures are acceptable?
  4. Is WaitingChildrenError imported correctly? (It's a class, not a string)
  5. Check getDependenciesCount() for unexpectedly zero values

5. Cross-Queue Dependency Mismatches

Symptom: Flow is added but children never get picked up. Parent stays in waiting-children indefinitely despite children appearing to exist.

Root cause: The FlowProducer.add() tree specifies queueName for each child, but the workers are watching different queue names (typo, different prefix, namespace mismatch).

// ❌ MISMATCH
const flow = await flowProducer.add({
  name: 'root',
  queueName: 'main-queue',     // parent queue
  children: [
    { name: 'child-1', queueName: 'child-worker-queue', data: {} }, // child queue
  ],
});

// Worker watching different queue:
const worker = new Worker('children-queue', /* … */); // ❌ TYPO: "children" vs "child"

Queue Hub's multi-backend support shows all queues and their active job counts at a glance. If a child queue has 0 active/0 waiting jobs but the parent is stuck, it's a strong signal the worker is on the wrong queue or not running.

Debugging with BullMQ's Built-in Introspection APIs

getDependencies() — The Swiss Army Knife

// Full paginated inspection of all dependency states
const deps = await job.getDependencies({
  processed: { count: 100, cursor: 0 },
  unprocessed: { count: 100, cursor: 0 },
  failed: { count: 100, cursor: 0 },
  ignored: { count: 100, cursor: 0 },
});

// Each returns { jobs: { [jobKey]: jobData }, cursor: string | 0 }

getDependenciesCount() — Quick Health Check

// Quick pulse check
const counts = await job.getDependenciesCount({
  failed: true,
  unprocessed: true,
});

if (counts.failed > 0) console.log(`${counts.failed} children failed`);
if (counts.failed === 0 && counts.unprocessed > 0) console.log('Children still running');
if (counts.failed === 0 && counts.unprocessed === 0) console.log('All children done');

getChildrenValues() — Collecting Results

// Returns { [jobKey: string]: any }
// jobKey format: "bull:queueName:jobId"
const values = await job.getChildrenValues();
for (const [key, value] of Object.entries(values)) {
  const [, queueName, jobId] = key.split(':');
  console.log(`Child ${jobId} on ${queueName} returned:`, value);
}

getIgnoredChildrenFailures() — Post-Mortem on Ignored Failures

// Only works if children were added with ignoreDependencyOnFailure: true
const failures = await job.getIgnoredChildrenFailures();
// Returns array of { jobId, queueName, failedReason, timestamp }
for (const f of failures) {
  console.warn(`Ignored child ${f.jobId} on ${f.queueName} failed: ${f.failedReason}`);
}

parentKey — Walking the Chain Upward

// Each job has a parentKey if it's a child in a flow
// Format: "bull:parentQueueName:parentJobId"
if (job.parentKey) {
  const parts = job.parentKey.split(':');
  const parentQueueName = parts[1];
  const parentJobId = parts[2];
  console.log(`Job ${job.id} is a child of ${parentJobId} on queue ${parentQueueName}`);
}

Using Queue Hub to Debug BullMQ Flow Workflows

Queue Hub provides visual tooling that replaces dozens of getDependencies() console.log calls with a single click.

Job Detail View — Full Flow Context

Queue Hub's job detail view shows every piece of flow metadata at a glance:

  • Job Statewaiting-children, waiting, active, completed, failed with timestamps
  • Parent Key — direct link to the parent job (click to navigate)
  • Dependency Counts — processed, unprocessed, failed, ignored — displayed as a visual badge
  • Children Queue — which queue each child belongs to (critical for cross-queue flows)
  • Return Values — what each child returned (no more manual getChildrenValues() debugging)
┌────────────────────────────────────────────┐
│  Job: complete-order (#job-abc-123)        │
│  Queue: orders                             │
│  State: waiting-children ⏳ (since 14:23)  │
│                                            │
│  ┌─ Dependencies ───────────────────────┐  │
│  │  ✅ 2 processed                      │  │
│  │  ❌ 1 failed   ← click to inspect   │  │
│  │  ⏳ 0 unprocessed                    │  │
│  └──────────────────────────────────────┘  │
│                                            │
│  ┌─ Failed Children ───────────────────┐   │
│  │  validate-payment (payments)        │   │
│  │  Error: Insufficient funds          │   │
│  │  [View Full Error] [Retry Child]    │   │
│  └──────────────────────────────────────┘  │
└────────────────────────────────────────────┘

Dependency Graph Visualization

Queue Hub renders the flow tree as an interactive directed graph, where you can:

  • Zoom and navigate deep chains (20+ levels)
  • Color-code by state — green = completed, red = failed, yellow = active, gray = waiting-children
  • Click any node to jump to that job's detail view
  • See dependency arrows showing the parent → child direction
  • Detect orphans — children whose parent is missing or has been removed
                ┌─────────────┐
                │ complete-   │
                │ order       │  ← waiting-children (1 failed)
                └──────┬──────┘
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
    ┌──────────┐ ┌──────────┐ ┌──────────┐
    │ validate │ │ check-   │ │ calculate│
    │ payment  │ │ inventory│ │ shipping │
    └─────┬────┘ └──────────┘ └──────────┘
          │
    ┌─────▼──────┐
    │ Insufficient│
    │ funds      │ ← 🔴 FAILED (blocking parent)
    └────────────┘

Live Queue Monitoring for Flow Workers

Queue Hub's live worker view shows which workers are actively processing flow children. For multi-queue flows, this surfaces:

  • Which queues have no active workers (children are never picked up)
  • Which workers are overloaded (children queuing up)
  • Job processing times per queue, helping identify bottlenecks

Advanced Flow Debugging Patterns

Writing a Flow Diagnostic Script

Keep this script ready in your codebase:

import { Queue, Job, FlowProducer } from 'bullmq';
import { Redis } from 'ioredis';

const connection = new Redis({ host: 'localhost', port: 6379, maxRetriesPerRequest: null });

interface FlowDiagnosticResult {
  healthy: boolean;
  parentState: string;
  childrenCounts: { processed: number; failed: number; unprocessed: number; ignored: number };
  childrenDetails: Array<{
    id: string;
    queueName: string;
    state: string;
    failedReason?: string;
    returnValue?: any;
  }>;
  chainLength?: number;
  recommendedAction?: string;
}

async function diagnoseFlow(parentJobId: string, queueName: string): Promise<FlowDiagnosticResult> {
  const queue = new Queue(queueName, { connection });
  const parent = await queue.getJob(parentJobId);

  if (!parent) {
    return { healthy: false, parentState: 'unknown', childrenCounts: { processed: 0, failed: 0, unprocessed: 0, ignored: 0 }, childrenDetails: [], recommendedAction: 'Parent job not found' };
  }

  const state = await parent.getState();
  const counts = await parent.getDependenciesCount();

  const deps = await parent.getDependencies({
    processed: { count: 100, cursor: 0 },
    failed: { count: 100, cursor: 0 },
    unprocessed: { count: 100, cursor: 0 },
    ignored: { count: 100, cursor: 0 },
  });

  const childrenDetails = [];

  for (const [key, data] of Object.entries(deps.failed || {})) {
    const [, , childQueue, childId] = key.split(':');
    childrenDetails.push({ id: childId, queueName: childQueue, state: 'failed', failedReason: data?.failedReason });
  }
  for (const [key] of Object.entries(deps.unprocessed || {})) {
    const [, , childQueue, childId] = key.split(':');
    childrenDetails.push({ id: childId, queueName: childQueue, state: 'unprocessed' });
  }
  for (const [key] of Object.entries(deps.processed || {})) {
    const [, , childQueue, childId] = key.split(':');
    childrenDetails.push({ id: childId, queueName: childQueue, state: 'processed' });
  }

  // Determine health
  const healthy = state !== 'waiting-children' || (counts.failed === 0 && counts.unprocessed === 0);

  // Recommended action
  let recommendedAction: string | undefined;
  if (state === 'waiting-children' && counts.failed > 0 && counts.unprocessed === 0) {
    recommendedAction = 'All children have finished but some failed. Either set ignoreDependencyOnFailure: true on child jobs, or retry the failed children.';
  } else if (state === 'waiting-children' && counts.unprocessed > 0) {
    recommendedAction = 'Children are still pending. Check if workers are running on the child queues.';
  }

  return { healthy, parentState: state, childrenCounts: counts, childrenDetails, recommendedAction };
}

// Usage:
// const result = await diagnoseFlow('job-abc-123', 'orders');
// console.log(result.healthy ? '✅ Healthy' : `❌ Issue: ${result.recommendedAction}`);

Automatic Deadlock Detection with Queue Hub Webhooks

For teams running flows in production, configure alerts when:

  • A job has been in waiting-children for more than X minutes
  • A failed child count exceeds a threshold
  • A completed parent has no childrenValues (unexpected empty chain)
  • Dependency count doesn't match expected children (some were lost)

Queue Hub's monitoring integrates with these signals, showing flow health at a glance across all your queues.

Flow Debugging Decision Tree

Is a parent job stuck?
│
├─ Is state "waiting-children"?
│  ├─ YES → Check dependency counts:
│  │  ├─ failed > 0, unprocessed = 0
│  │  │  └─ 🔴 All children finished but some failed
│  │  │     → Add ignoreDependencyOnFailure: true
│  │  │     → Or retry failed children
│  │  │
│  │  ├─ unprocessed > 0
│  │  │  └─ 🔴 Children still pending
│  │  │     → Are workers running on child queues?
│  │  │     → Are child queue names correct?
│  │  │     → Use Queue Hub to check worker status
│  │  │
│  │  └─ processed === 0, failed === 0
│  │     └─ 🔴 No children exist at all
│  │        → FlowProducer.add() may have failed silently
│  │        → Check if children were added atomically
│  │
│  └─ NO → Is parent in "completed" with no children values?
│     └─ 🟡 Check if children returned values
│        → Verify worker returns data from processor
│
├─ Is parent stuck in "waiting-children" with dynamic children pattern?
│  └─ Are children added with opts.parent correctly set?
│     → Verify parent.id and parent.queue
│     → Check moveToWaitingChildren() called after adding children
│     → Ensure WaitingChildrenError is imported
│
└─ Is a deep chain broken?
   └─ Walk the chain upward using parentKey
      → TraceChain() utility (see above)
      → Find the first job in "waiting-children" or "failed"

Best Practices for Flow Producer Workflows

  1. Always set ignoreDependencyOnFailure: true on non-critical children — unless the parent genuinely must not proceed without every child, make individual child failures non-blocking.

  2. Add a flow TTL — use opts.ttl on parent jobs so stuck flows eventually expire:

    await flowProducer.add({
      name: 'expiring-flow',
      queueName: 'flows',
      opts: { ttl: 3600000 }, // Auto-clean after 1 hour
      children: [/* … */],
    });
    
  3. Log at flow boundaries — in every parent worker, log:

    const counts = await job.getDependenciesCount();
    console.log(`Flow ${job.id}: processing with ${counts.processed} processed, ${counts.failed} failed`);
    
  4. Use getIgnoredChildrenFailures() in parent workers — don't silently swallow child failures:

    const failures = await job.getIgnoredChildrenFailures();
    if (failures.length > 0) {
      console.warn(`Flow ${job.id}: ${failures.length} children failed but were ignored`);
      // Send to your error tracking system
    }
    
  5. Keep flows shallow — deep nesting (5+ levels) is hard to debug. If your chain is deeper, consider splitting into separate orchestrator flows.

  6. Use Queue Hub's dependency graph — don't wait for a crisis. Regularly inspect the flow topology of your production queues to catch structural issues early.

FAQ — Flow Debugging Edition

Q: My parent job is stuck in waiting-children but all children completed successfully. What gives? A: Check if children were completed with a failed return value even though the processor ran. BullMQ distinguishes between "processed" and "failed" — a child that throws but has retries left may be in retry, not completed. Use getDependencies() to inspect the exact status.

Q: Can a child job belong to a different queue than its parent? A: Yes! FlowProducer supports cross-queue flows. The child's queueName determines its queue. The parent waits regardless of where children live. Use Queue Hub's multi-backend support to see all queues involved in a flow at once.

Q: How do I retry an entire flow from scratch vs from the failed child? A: BullMQ doesn't have a "retry flow" primitive. To retry from the failed child, retry that specific child job. To retry the entire flow, remove the flow and re-add it via FlowProducer.add(). Queue Hub's job detail view lets you retry individual children with one click.

Q: What happens if I remove a child job manually? A: Removing a child job from Redis does not update the parent's dependency list. The parent will wait indefinitely for a child that no longer exists. Always remove flows through BullMQ APIs (job.remove()), not by deleting Redis keys directly. Queue Hub's UI uses the correct APIs to prevent this.

Q: Can a child have multiple parents? A: No. BullMQ's flow model is a tree (each child has exactly one parent), not a DAG (a child cannot have multiple parents). For fan-in/multi-parent patterns, use an intermediary orchestrator job.

Conclusion — From "Where's My Flow?" to "I See It All"

BullMQ's Flow Producer is one of the most powerful features for modeling complex business processes as job workflows. But with that power comes a new category of debugging challenges — invisible dependency deadlocks, silent chain breaks, and stuck parent jobs that halt your entire pipeline.

By understanding the waiting-children lifecycle, using ignoreDependencyOnFailure intentionally, and leveraging introspection APIs like getDependencies() and getDependenciesCount(), you can debug any flow failure in minutes rather than hours.

And when you're tired of typing await job.getDependencies() over and over — Queue Hub turns every flow into a visual, clickable graph.

Try Queue Hub

Stop guessing which child failed. Stop console.logging dependency counts. Get a live, interactive dependency graph of every BullMQ flow in your system.

Queue Hub gives you:

  • 🔍 Job Detail View — full flow context: parent key, dependency counts, child failures, and return values
  • 🌳 Dependency Graph — interactive visualization of parent-child trees, color-coded by state
  • 📊 Live Worker View — see which workers are processing flow children in real time
  • 🔗 Multi-Backend — connect multiple Redis instances, see cross-queue flows in one dashboard
  • 🚀 Agent Tunneling — secure access to private Redis behind VPCs without exposing ports
  • 👥 Multi-Org — share flow debugging with your team

Start debugging flows in minutes, not hours.queuehub.dev — Free tier available. No credit card required.


Appendix: Quick-Reference Flow Debugging API

API Returns Use Case
job.getState() 'waiting-children' | 'waiting' | 'active' | 'completed' | 'failed' Check where a job is in its lifecycle
job.getDependencies() { processed, unprocessed, failed, ignored } Inspect which specific children are in each state
job.getDependenciesCount() { processed: number, failed: number, unprocessed: number, ignored: number } Quick health check of a flow
job.getChildrenValues() { [jobKey]: returnValue } Collect results from all completed children
job.getIgnoredChildrenFailures() Array<{ jobId, queueName, failedReason }> Post-mortem on children that failed with ignoreDependencyOnFailure
job.parentKey string | undefined Get the parent's fully qualified key to walk the chain upward
job.moveToWaitingChildren() Promise<boolean> Tell BullMQ the parent has added children and should wait (dynamic flows)
WaitingChildrenError Error class Throw in a processor to yield the parent job back to waiting-children

Written for the Queue Hub engineering blog. First published July 2026.

Related Articles