Skip to main content
ExuluQueues is exported as a singleton from @exulu/backend. It wraps BullMQ and Redis to provide rate-limited, concurrency-controlled background job queues. You register named queues against it, then pass the queue configuration into the parts of your app that need background work — the embedder inside an ExuluContext, a context source, a context processor, or an ExuluEval runner.
ExuluQueues requires an Enterprise Edition license. Calling register() without a valid EXULU_ENTERPRISE_LICENSE environment variable throws immediately.

What a queue gives you

BullMQ / Redis

Each registered queue is a BullMQ Queue instance backed by Redis. Jobs persist across worker restarts.

Rate limiting

ratelimit caps how many jobs per second each worker processes — the right lever for embedding provider quotas.

Two concurrency tiers

concurrency.worker sets the per-process concurrency; concurrency.queue sets a global cap enforced by BullMQ across all worker processes.

Fail-fast startup

Lazy Redis connection with a hard 60-second startup timeout — IMP aborts rather than hanging silently when Redis is unreachable.

OpenTelemetry

BullMQ-Otel instrumentation is wired automatically; job duration, queue depth, and error rates appear in your observability stack.

GraphQL surface

Registered queue names populate QueueEnum in the GraphQL schema, exposing job inspection and management queries.

Minimal example

How queues are consumed

Registered queues surface in three places inside ExuluContext: The ExuluEval queue option uses the same ExuluQueueConfig shape — pass ExuluQueues.register(...).use() there too. Every registered queue name is injected into the GraphQL QueueEnum at boot time, enabling the IMP platform’s job inspection UI without extra configuration.

Redis connection and fail-fast behaviour

register() is synchronous and does not touch Redis immediately. The actual Redis connection is made lazily when .use() is first awaited. At that point:
  1. IMP checks that REDIS_HOST and REDIS_PORT are set. If either is missing, .use() throws before touching the network.
  2. A new BullMQ Queue is opened with enableOfflineQueue: false.
  3. guardRedisStartup() races queue.waitUntilReady() against a 60-second timeout. If Redis does not become reachable within 60 seconds, .use() rejects with a clear error that cites the configured address and the last surfaced connection error.
  4. On success, the queue is pushed into ExuluQueues.queues and returned. Subsequent calls to .use() for the same queue name skip the connection step and return the cached instance.
Set the Redis target with environment variables (read once at import time):

Concurrency and rate-limit sizing

register() accepts two independent knobs:
  • concurrency.worker — how many jobs a single Node.js worker process runs in parallel. For I/O-bound work like API calls, 5–20 is typical. For CPU-heavy work, stay close to 1.
  • concurrency.queue — the global cap enforced by BullMQ via setGlobalConcurrency. Keeps total active jobs bounded even when you run multiple worker processes.
  • ratelimit — maximum jobs per second across a worker process. Set this to stay under your embedding provider’s tokens-per-minute (TPM) quota.
A rough sizing method for the embedder queue: divide your provider’s TPM quota by the average token count per embedding request. For example, a 5 M TPM quota embedding 1 024-token batches supports roughly 5 000 000 ÷ 1 024 ≈ 4 882 requests per minute, or about 81 per second — set ratelimit to that value and choose concurrency.queue to match your desired burst depth. Start conservative and scale up after observing queue depth in your telemetry.

Next steps

Configuration

Every register() parameter and ExuluQueueConfig field.

API reference

register(), queue(), .list, and .queues — full signatures.