BullMQ Worker Lifecycle#
Overview#
All BullMQ queue workers in the Langfuse worker service are managed by the WorkerManager singleton class. It handles registration, per-job metric instrumentation, error event binding, and graceful shutdown. The Express app at worker/src/app.ts owns the startup sequence: it calls WorkerManager.register() once per enabled queue (guarded by QUEUE_CONSUMER_*_IS_ENABLED env vars), then registers onShutdown for SIGINT/SIGTERM .
Registration#
WorkerManager.register(queueName, processor, options?) is the single entry point for bringing a worker online:
- Deduplicates — if a worker for that queue already exists, it logs and returns .
- Creates a dedicated Redis connection via
createNewRedisInstance(redisQueueRetryOptions). - Wraps the caller-supplied processor with
metricWrapperbefore passing it to BullMQ'sWorkerconstructor . - Binds
"failed"and"error"event listeners that log, calltraceException, and increment OTel counters .
Workers are registered conditionally in app.ts based on environment variables. Each queue section looks like:
if (env.QUEUE_CONSUMER_<QUEUE>_IS_ENABLED === "true") {
WorkerManager.register(QueueName.<Queue>, processor, { concurrency, limiter? });
}
Sharded queues (Ingestion, OtelIngestion, TraceUpsert, EvalExecution, LLMAsJudge) iterate over Queue.getShardNames() and register one worker per shard .
Queue Metrics#
WorkerManager.metricWrapper wraps every job with OTel instrumentation:
- On entry: increments
<queue>.requestand records<queue>.wait_time(job age since enqueue) . - On completion: samples queue-depth gauges (
<queue>.lengthwaiting,<queue>.dlq_lengthfailed,<queue>.active) and records<queue>.processing_time. - Sharded queues sample depth gauges probabilistically via
LANGFUSE_QUEUE_METRICS_SAMPLE_RATEto reduce metric volume .
Stall Handling#
Long-running jobs (eval execution, PostHog/Mixpanel/BlobStorage integration processing, trace delete) use non-default stall settings to tolerate slow processors :
| Setting | Value | Reason |
|---|---|---|
lockDuration | 60 000 ms | Reduces lock-renewal frequency; more tolerant of CPU wait spikes |
stalledInterval | 120 000 ms | Checks for stalled jobs every 2 min instead of every 30 s |
maxStalledCount | 3 | Allows up to 3 stall recoveries before moving to failed (default is 1) |
These settings are applied to the EvalExecution family, LLMAsJudge, PostHog/Mixpanel/BlobStorage integration processing, and TraceDelete queues.
Health Check & Readiness Endpoints#
worker/src/api/index.ts mounts two routes on the /api prefix:
| Route | failOnSigterm | Purpose |
|---|---|---|
GET /api/health | false | Liveness — checks Postgres (SELECT 1) and Redis (PING with 2 s timeout) |
GET /api/ready | true | Readiness — same checks, plus returns 500 if SIGTERM has been received |
checkContainerHealth performs the actual checks. The SIGTERM guard lets Kubernetes stop routing traffic as soon as shutdown begins without waiting for the process to exit.
Stuck Worker (Event Propagation) Health Check#
Added in PR #14680, both /health and /ready accept an optional query param ?failIfEventPropagationStuck=true. When set:
- The
EventPropagationQueueworker writes a Redis heartbeat key (langfuse:event-propagation:last-run-started-at) at the start of every job invocation, including no-op runs . evaluateEventPropagationStuckinhealth/index.tsreads the key and compares staleness againstLANGFUSE_EVENT_PROPAGATION_STUCK_THRESHOLD_MINUTES(default 15 min — sized above the 10-min ClickHouse INSERT timeout).- If stale beyond the threshold, the endpoint returns 503. A Kubernetes liveness probe configured with this param will trigger a pod restart, which frees the occupied global-concurrency slot.
- If the heartbeat key is absent (job never ran), the endpoint returns 200 (not stuck) to avoid crash-looping a fresh pod.
The response body includes eventPropagation.propagationDelayMs and eventPropagation.lastRunStartedAt for observability, but propagation delay alone does not drive the 503 — a growing backlog usually means slow ClickHouse, not a stuck worker.
Shutdown Sequence#
SIGINT / SIGTERM both invoke onShutdown:
setSigtermReceived()— marks the readiness probe as failing immediately .server.close()— stops accepting new HTTP connections .- Batch cleaners are stopped .
WorkerManager.closeWorkers()— callsworker.close()on all registered workers in parallel , following BullMQ production guidance.BackgroundMigrationManager.close()— aborts active migrations .ClickhouseWriter.getInstance().shutdown()— flushes pending buffered writes after workers are closed to prevent new writes mid-flush .- Redis, Prisma, ClickHouse connections are closed; tokenization worker threads are terminated and tokenizer cache freed .
Key Files#
| File | Role |
|---|---|
worker/src/queues/workerManager.ts | WorkerManager — register, metric-wrap, close workers |
worker/src/app.ts | Startup: conditionally registers all workers and cleaners |
worker/src/api/index.ts | /api/health and /api/ready route handlers |
worker/src/features/health/index.ts | checkContainerHealth, SIGTERM flag, evaluateEventPropagationStuck |
worker/src/utils/shutdown.ts | onShutdown — ordered teardown of all subsystems |