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:
- IMP checks that
REDIS_HOSTandREDIS_PORTare set. If either is missing,.use()throws before touching the network. - A new BullMQ
Queueis opened withenableOfflineQueue: false. guardRedisStartup()racesqueue.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.- On success, the queue is pushed into
ExuluQueues.queuesand returned. Subsequent calls to.use()for the same queue name skip the connection step and return the cached instance.
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 viasetGlobalConcurrency. 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.
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.