Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt

Use this file to discover all available pages before exploring further.

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 BLPOP / BRPOPLPUSH. 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

WorkloadRecommended mode
Free-tier Redis or low-traffic playgroundCron-driven, every 5 min
Production with paid Redis + active batch/journey trafficAlways-on
Mixed (paid Redis, but workers run elsewhere as a separate service)Cron-driven on the API; standalone worker container for the heavy queues

Set the toggle

Set the env var on your API container and redeploy:
# Disables in-process workers (instrumentation.ts skips importing src/worker/index.ts)
WORKER_INPROCESS=0
# Required — used for cron auth
CRON_SECRET=<long-random-hex>
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

Header: X-Cron-Secret: <CRON_SECRET> (or Authorization: Bearer <CRON_SECRET>). The endpoint also accepts CRON_TOKEN for backwards compatibility on environments that haven’t migrated.

Query parameters

ParameterTypeDefaultDescription
queuestring(all 5)Drain only this queue. Allowed: batch-jobs, dsar-jobs, journey-jobs, retrain-jobs, seed-jobs.
maxDurationMsnumber60000 (1 min)Hard wall-clock cap. Max 600000 (10 min). The endpoint exits as soon as queues report idle, but never runs longer than this.
maxConcurrentQueuesnumber2How many queues run BullMQ workers in parallel inside one invocation. Caps concurrent Redis connections so idle ops stay low on free-tier Redis. Bump to 5 when self-hosted.
maxJobsPerQueuenumberunboundedSafety stop per queue. Useful when chaining short cron ticks.

Response 200

{
  "ok": true,
  "durationMs": 1234,
  "totalProcessed": 3,
  "totalFailed": 0,
  "queues": {
    "batch-jobs":   { "jobsProcessed": 1, "jobsFailed": 0, "idleAt": 412 },
    "dsar-jobs":    { "jobsProcessed": 0, "jobsFailed": 0, "idleAt": 0 },
    "journey-jobs": { "jobsProcessed": 2, "jobsFailed": 0, "idleAt": 891 },
    "retrain-jobs": { "jobsProcessed": 0, "jobsFailed": 0, "idleAt": 0 },
    "seed-jobs":    { "jobsProcessed": 0, "jobsFailed": 0, "idleAt": 0 }
  }
}
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

CodeReason
400Unknown queue parameter.
401Missing or invalid CRON_SECRET.
500REDIS_URL not configured.

Scheduling — pick one

Option A — GitHub Actions cron (simplest, free)

.github/workflows/drain-queues.yml:
name: Drain Queues
on:
  schedule:
    - cron: "*/5 * * * *"   # every 5 minutes
  workflow_dispatch:
jobs:
  drain:
    runs-on: ubuntu-latest
    steps:
      - run: |
          curl -fsS -X POST "${{ secrets.PLAYGROUND_URL }}/api/v1/cron/drain-queues" \
            -H "X-Cron-Secret: ${{ secrets.CRON_SECRET }}" \
            -H "Content-Type: application/json"
GitHub’s free tier gives 2,000 minutes/month — plenty for 5-min cron.

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

aws events put-rule --name kaireon-drain-queues \
  --schedule-expression "rate(5 minutes)" \
  --region us-east-1

aws events put-targets --rule kaireon-drain-queues \
  --targets '[{
    "Id": "1",
    "Arn": "arn:aws:apigateway:us-east-1:apigateway/.../api/v1/cron/drain-queues",
    "HttpParameters": {
      "HeaderParameters": { "X-Cron-Secret": "<CRON_SECRET>" }
    }
  }]'
(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: 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:
curl -fsS -X POST "$KAIREON_URL/api/v1/cron/drain-queues" \
  -H "X-Cron-Secret: $CRON_SECRET"

Cost math

For a tenant with zero queued jobs (the common idle case on playground):
ops_per_invocation = 5 queues × 4 ops (getJobCounts) = 20
invocations_per_month = 12 × 24 × 30 = 8,640  (every 5 min)
ops_per_month_idle = ~173,000

Free tier (Upstash) = 500,000 ops/month
Headroom = ~327,000 ops for actual jobs + /recommend rate-limit + flow cache
For every-30-minute cron (lower latency tolerance):
ops_per_month_idle = 20 × 48 × 30 = ~29,000
Headroom = ~471,000 ops for everything else
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).