Skip to main content
KaireonAI’s BullMQ workers have two execution modes:
  1. Always-on (WORKER_INPROCESS=1, the legacy default): five workers run continuously inside the API container, long-polling Redis with blocking pop operations. Job latency ≈ 0. Idle Redis cost ≈ 30 ops/min permanently.
  2. Cron-driven drain (WORKER_INPROCESS=0, recommended for free-tier Redis): no always-on workers. A scheduled cron hits POST /api/v1/cron/drain-queues every few minutes. Each invocation connects, processes available jobs, and disconnects. Job latency ≤ cron interval. Idle Redis cost ≈ 20 ops per invocation × invocations/day.
For a 5-minute cron with 5 idle queues, that’s ~170K ops/month — comfortably under the Upstash free tier of 500K, vs ~1.3M/month for always-on workers with no actual jobs.

When to use

Set the toggle

Set the env var on your API container and redeploy:
When WORKER_INPROCESS is unset or =1, the API container runs the legacy always-on worker. When =0, only /api/v1/cron/drain-queues produces job consumption.

POST /api/v1/cron/drain-queues

Drain queued jobs across the 5 BullMQ queues (batch-jobs, dsar-jobs, journey-jobs, retrain-jobs, seed-jobs).

Auth

Three env vars are accepted in priority order: Headers accepted: X-Cron-Secret, X-Cron-Token, or Authorization: Bearer <token>. Hardening (also active by default):
  • Sliding-window per-IP rate limitDRAIN_QUEUES_RATE_LIMIT requests/minute (default 12). Returns 429 with Retry-After when exceeded.
  • Optional IP allowlist — set CRON_ALLOWED_IPS=ip1,ip2,… (comma-separated). When set, only requests from listed IPs (matched against X-Forwarded-For left-most) pass. When unset, IP check is skipped (token-only).
  • Constant-time token compare — prevents timing-attack token discovery.

Query parameters

Response 200

idleAt is the wall-clock-ms-since-start when the queue first reported idle. 0 means the queue was already empty when probed (no worker was started — the cheap getJobCounts probe runs and the endpoint moves on).

Error codes

Scheduling — pick one

Option A — GitHub Actions cron (simplest, free)

.github/workflows/drain-queues.yml:
GitHub’s free tier gives 2,000 minutes/month — plenty for 5-min cron.

Option B — AWS EventBridge schedule (preferred when already on AWS)

(For App Runner, you may need an API Gateway connector or a Lambda intermediary since EventBridge can’t directly POST to App Runner URLs.)

Option C — External uptime monitor (cheap, hands-off)

Services like Cron-Job.org, EasyCron, or UptimeRobot can hit any HTTPS URL on a schedule. Configure:
  • URL: https://playground.kaireonai.com/api/v1/cron/drain-queues
  • Method: HTTP POST
  • Headers: X-Cron-Secret: <CRON_SECRET>
  • Schedule: */5 * * * *

Option D — Self-managed (k8s CronJob, supervised cron, etc.)

Use whatever scheduler your platform provides. Each tick should run:

Cost math

For a tenant with zero queued jobs (the common idle case on playground):
For every-30-minute cron (lower latency tolerance):
In practice you can run a 5-min cron on a free-tier Redis with no concerns until you reach hundreds of /recommend calls per minute, at which point the rate-limiter cost dominates and you should upgrade Redis anyway.

Caveats

  • Job-failure semantics differ from always-on: in always-on mode, a failed job retries via BullMQ’s exponential backoff immediately. In cron-driven mode, retries are picked up on the next tick. For low-frequency workloads this is fine. For SLA-sensitive workloads, run always-on workers on paid Redis.
  • Long-running jobs (>maxDurationMs) will be aborted mid-flight when the worker closes. They’ll be re-enqueued by BullMQ’s stalled-job detector on the next tick. Set maxDurationMs higher than your longest expected job, or split jobs into smaller chunks.
  • The drain endpoint is idempotent — re-hitting it during an in-flight invocation just no-ops on jobs already in-flight (BullMQ’s lock semantics).