Skip to main content
IMP’s background work — embedding generation, document processing, data-source syncs, and eval runs — all run on BullMQ queues backed by Redis. This tutorial shows you how to size queues to a rate limit, register them in code, and scope a worker process to a subset of queues in production.

Prerequisites

ExuluQueues requires an Enterprise Edition license. register() throws immediately without a valid EXULU_ENTERPRISE_LICENSE environment variable.

What you will build

  • A sized embedder queue for a 2 M TPM embedding provider.
  • A processor queue for document conversion jobs.
  • A worker process scoped to only the embedder queue (so you can scale embedder workers independently).
1

Understand the sizing formula

The ratelimit parameter limits how many jobs a single worker process dispatches per second. For embedding queues, the right value comes from your provider’s tokens-per-minute (TPM) quota:
Example — a provider with a 2 M TPM quota embedding 1 024-token chunks:
For concurrency.queue, pick a number that represents the burst depth you want in Redis. A conservative starting point is ratelimit × 2 (enough to keep the queue fed):
For concurrency.worker, use the number of parallel I/O operations you want per process. Embedding calls are network-bound, so 5–10 is typical:
ratelimit is per-process. If you run multiple worker processes, the effective rate is ratelimit × number_of_processes. Use concurrency.queue to cap total active jobs globally and bound the combined rate.
2

Register the queues

Register all queues in src/contexts/index.ts (or a dedicated src/queues.ts file). Queues must be registered before the context or eval that references them is constructed:
src/queues.ts
Pass the queue to the context by calling .use():
src/contexts/index.ts
.use() connects to Redis lazily on first await — the call itself is synchronous and returns a Promise<ExuluQueueConfig>. Passing the promise (not the awaited value) to the context lets IMP wire it up when the worker starts.
3

Scope a worker to specific queues

By default, app.bullmq.workers.create() starts workers for every registered queue. For production deployments, you often want separate containers for separate queue types — for example, high-memory document processors on beefy nodes and many small embedder workers on lighter ones.Pass queue names to workers.create() to restrict processing to a subset:
src/worker.ts (embedder worker)
src/worker-processor.ts (processor worker)
Add corresponding scripts to package.json:
In your tsup config, add the processor worker as an entry:
tsup.config.ts
4

Memory guidance

PDF processing and embedding generation both use significant memory. As a starting point:Document processing workers that load large PDFs or run OCR models can use 4–8 GB. Allocate accordingly in your container scheduler. See Operations for container sizing recommendations.Set concurrency.worker: 1 for document processing workers if memory is constrained — running two large-PDF jobs in parallel doubles peak usage.
5

Verify queue health

After starting your worker, registered queue names appear in the IMP admin interface under Build → Knowledge → [context] → Pipeline → [stage] → Jobs. You can also inspect queue depths, drain queues, and retry failed jobs from there.Every registered queue name is injected into the GraphQL QueueEnum at boot, making queue state queryable via the platform’s API without additional configuration.

What you built

  • A sized embedder queue based on a TPM quota formula.
  • Separate queues for embedding, processing, and sync.
  • A worker entry point scoped to a single queue type, ready for independent horizontal scaling.

Next steps

ExuluQueues — configuration

Every register() parameter, the ExuluQueueConfig shape, and rate-limit sizing examples.

Operations

Container sizing, memory guidance, and production deployment patterns.

Custom processors

Attach a processor queue to a context and configure generateEmbeddings.