{"id":42599177,"url":"https://github.com/egeominotti/bunqueue","last_synced_at":"2026-04-12T21:19:22.909Z","repository":{"id":335178064,"uuid":"1144620658","full_name":"egeominotti/bunqueue","owner":"egeominotti","description":"⚡ High-performance job queue for Bun. SQLite persistence, DLQ, cron jobs, S3 backups. Built for AI agents and automation  ","archived":false,"fork":false,"pushed_at":"2026-03-12T19:52:36.000Z","size":5050,"stargazers_count":354,"open_issues_count":1,"forks_count":7,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-13T01:48:23.105Z","etag":null,"topics":["ai","ai-agents","ai-scheduler","background-jobs","bullmq-alternative","bun","bun-queue","cron-scheduler","job-queue","message-queue","queue","redis-alternative","s3","sqlite3","task-queue-typescript"],"latest_commit_sha":null,"homepage":"https://bunqueue.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/egeominotti.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":".github/SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-28T21:26:22.000Z","updated_at":"2026-03-12T19:39:48.000Z","dependencies_parsed_at":"2026-03-07T18:01:06.375Z","dependency_job_id":null,"html_url":"https://github.com/egeominotti/bunqueue","commit_stats":null,"previous_names":["egeominotti/bunq"],"tags_count":240,"template":false,"template_full_name":null,"purl":"pkg:github/egeominotti/bunqueue","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/egeominotti%2Fbunqueue","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/egeominotti%2Fbunqueue/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/egeominotti%2Fbunqueue/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/egeominotti%2Fbunqueue/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/egeominotti","download_url":"https://codeload.github.com/egeominotti/bunqueue/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/egeominotti%2Fbunqueue/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30596295,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-16T23:03:25.146Z","status":"ssl_error","status_checked_at":"2026-03-16T23:01:15.935Z","response_time":96,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","ai-agents","ai-scheduler","background-jobs","bullmq-alternative","bun","bun-queue","cron-scheduler","job-queue","message-queue","queue","redis-alternative","s3","sqlite3","task-queue-typescript"],"created_at":"2026-01-29T00:16:52.561Z","updated_at":"2026-04-12T21:19:22.896Z","avatar_url":"https://github.com/egeominotti.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://bunqueue.dev/\"\u003e\n    \u003cimg src=\".github/banner.svg\" alt=\"bunqueue\" width=\"400\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/bunqueue\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/bunqueue?style=flat-square\" alt=\"npm version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/bunqueue\"\u003e\u003cimg src=\"https://img.shields.io/npm/dm/bunqueue?style=flat-square\" alt=\"npm downloads\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/egeominotti/bunqueue/actions\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/egeominotti/bunqueue/ci.yml?style=flat-square\u0026label=CI\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/egeominotti/bunqueue/stargazers\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/egeominotti/bunqueue?style=flat-square\" alt=\"GitHub Stars\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/egeominotti/bunqueue/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/egeominotti/bunqueue?style=flat-square\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  High-performance job queue for Bun. Built for AI agents and automation.\u003cbr/\u003e\n  Zero external dependencies. MCP-native. TypeScript-first.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://bunqueue.dev/\"\u003eDocumentation\u003c/a\u003e \u0026middot;\n  \u003ca href=\"https://bunqueue.dev/guide/benchmarks/\"\u003eBenchmarks\u003c/a\u003e \u0026middot;\n  \u003ca href=\"https://www.npmjs.com/package/bunqueue\"\u003enpm\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## Quickstart\n\n```bash\nbun add bunqueue\n```\n\n```typescript\nimport { Bunqueue } from 'bunqueue/client';\n\nconst app = new Bunqueue('emails', {\n  embedded: true,\n  processor: async (job) =\u003e {\n    console.log(`Sending to ${job.data.to}`);\n    return { sent: true };\n  },\n});\n\nawait app.add('send', { to: 'alice@example.com' });\n```\n\nThat's it. Queue + Worker in one object. No Redis, no config, no setup.\n\n---\n\n## Simple Mode\n\nSimple Mode gives you a Queue and a Worker in a single object. Add jobs, process them, add middleware, schedule crons — all from one place.\n\nUse `Bunqueue` when producer and consumer are in the same process. For distributed systems, use `Queue` + `Worker` separately. For AI agent workflows, use the [MCP Server](#built-for-ai-agents-mcp-server) instead — agents control queues via natural language without writing code.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eArchitecture\u003c/b\u003e\u003c/summary\u003e\n\n```\nnew Bunqueue('emails', opts)\n    │\n    ├── this.queue  = new Queue('emails', ...)\n    ├── this.worker = new Worker('emails', ...)\n    │\n    └── Subsystems (all optional):\n        ├── RetryEngine         ── jitter, fibonacci, exponential, custom\n        ├── CircuitBreaker      ── pauses worker after N failures\n        ├── BatchAccumulator    ── groups N jobs into one call\n        ├── TriggerManager      ── \"on complete → create job B\"\n        ├── TtlChecker          ── rejects expired jobs\n        ├── PriorityAger        ── boosts old jobs' priority\n        ├── CancellationManager ── AbortController per job\n        └── DedupDebounceMerger ── deduplication \u0026 debounce\n```\n\nProcessing pipeline per job:\n\n```\nJob → Circuit Breaker → TTL check → AbortController → Retry → Middleware → Processor\n```\n\n\u003c/details\u003e\n\n### Routes\n\nRoute jobs to different handlers by name:\n\n```typescript\nconst app = new Bunqueue('notifications', {\n  embedded: true,\n  routes: {\n    'send-email': async (job) =\u003e {\n      await sendEmail(job.data.to);\n      return { channel: 'email' };\n    },\n    'send-sms': async (job) =\u003e {\n      await sendSMS(job.data.to);\n      return { channel: 'sms' };\n    },\n  },\n});\n\nawait app.add('send-email', { to: 'alice' });\nawait app.add('send-sms', { to: 'bob' });\n```\n\n\u003e **Note:** Use one of `processor`, `routes`, or `batch`. Passing multiple or none throws an error.\n\n### Middleware\n\nWraps every job execution. Execution order is onion-style: `mw1 → mw2 → processor → mw2 → mw1`. When no middleware is added, zero overhead.\n\n```typescript\n// Timing middleware\napp.use(async (job, next) =\u003e {\n  const start = Date.now();\n  const result = await next();\n  console.log(`${job.name}: ${Date.now() - start}ms`);\n  return result;\n});\n\n// Error recovery middleware\napp.use(async (job, next) =\u003e {\n  try {\n    return await next();\n  } catch (err) {\n    return { recovered: true, error: err.message };\n  }\n});\n```\n\n### Batch Processing\n\nAccumulates N jobs and processes them together. Flushes when buffer reaches `size` or `timeout` expires. On `close()`, remaining jobs are flushed.\n\n```typescript\nconst app = new Bunqueue('db-inserts', {\n  embedded: true,\n  batch: {\n    size: 50,\n    timeout: 2000,\n    processor: async (jobs) =\u003e {\n      const rows = jobs.map(j =\u003e j.data.row);\n      await db.insertMany('table', rows);\n      return jobs.map(() =\u003e ({ inserted: true }));\n    },\n  },\n});\n```\n\n### Advanced Retry\n\n5 strategies + retry predicate:\n\n```typescript\nconst app = new Bunqueue('api-calls', {\n  embedded: true,\n  processor: async (job) =\u003e {\n    const res = await fetch(job.data.url);\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return { status: res.status };\n  },\n  retry: {\n    maxAttempts: 5,\n    delay: 1000,\n    strategy: 'jitter',\n    retryIf: (error) =\u003e error.message.includes('503'),\n  },\n});\n```\n\n| Strategy | Formula | Use case |\n| --- | --- | --- |\n| `fixed` | `delay` every time | Rate-limited APIs |\n| `exponential` | `delay × 2^attempt` | General purpose |\n| `jitter` | `delay × 2^attempt × random(0.5-1.0)` | Thundering herd prevention |\n| `fibonacci` | `delay × fib(attempt)` | Gradual backoff |\n| `custom` | `customBackoff(attempt, error) → ms` | Anything |\n\n\u003e This is in-process retry — the job stays active. Different from core `attempts`/`backoff` which re-queues.\n\n### Graceful Cancellation\n\nCancel running jobs with AbortController:\n\n```typescript\nconst app = new Bunqueue('encoding', {\n  embedded: true,\n  processor: async (job) =\u003e {\n    const signal = app.getSignal(job.id);\n    for (const chunk of chunks) {\n      if (signal?.aborted) throw new Error('Cancelled');\n      await encode(chunk);\n    }\n    return { done: true };\n  },\n});\n\nconst job = await app.add('video', { file: 'big.mp4' });\napp.cancel(job.id);        // cancel immediately\napp.cancel(job.id, 5000);  // cancel after 5s grace period\n```\n\nThe signal works with `fetch` too: `await fetch(url, { signal })`.\n\n### Circuit Breaker\n\nPauses the worker after too many consecutive failures:\n\n```\nCLOSED ──→ failures ≥ threshold ──→ OPEN (worker paused)\n                                       │\n              ←── success ──── HALF-OPEN ←── timeout expires\n```\n\n```typescript\nconst app = new Bunqueue('payments', {\n  embedded: true,\n  processor: async (job) =\u003e paymentGateway.charge(job.data),\n  circuitBreaker: {\n    threshold: 5,\n    resetTimeout: 30000,\n    onOpen: () =\u003e alert('Gateway down!'),\n    onClose: () =\u003e alert('Gateway recovered'),\n  },\n});\n\napp.getCircuitState();  // 'closed' | 'open' | 'half-open'\napp.resetCircuit();     // force close + resume worker\n```\n\n### Event Triggers\n\nCreate follow-up jobs automatically when a job completes or fails:\n\n```typescript\nconst app = new Bunqueue('orders', {\n  embedded: true,\n  routes: {\n    'place-order': async (job) =\u003e ({ orderId: job.data.id, total: 99 }),\n    'send-receipt': async (job) =\u003e ({ sent: true }),\n    'fraud-alert': async (job) =\u003e ({ alerted: true }),\n  },\n});\n\napp.trigger({\n  on: 'place-order',\n  create: 'send-receipt',\n  data: (result, job) =\u003e ({ id: job.data.id }),\n});\n\n// Conditional trigger (only for large orders)\napp.trigger({\n  on: 'place-order',\n  create: 'fraud-alert',\n  data: (result) =\u003e ({ amount: result.total }),\n  condition: (result) =\u003e result.total \u003e 1000,\n});\n\n// Chain triggers\napp\n  .trigger({ on: 'step-1', create: 'step-2', data: (r) =\u003e r })\n  .trigger({ on: 'step-2', create: 'step-3', data: (r) =\u003e r });\n```\n\n### Job TTL\n\nExpire unprocessed jobs. Checked when the worker picks up the job:\n\n```typescript\nconst app = new Bunqueue('otp', {\n  embedded: true,\n  processor: async (job) =\u003e verifyOTP(job.data.code),\n  ttl: {\n    defaultTtl: 300000,\n    perName: {\n      'verify-otp': 60000,\n      'daily-report': 0,\n    },\n  },\n});\n\napp.setDefaultTtl(120000);\napp.setNameTtl('flash-sale', 30000);\n```\n\nResolution: `perName[job.name]` → `defaultTtl` → `0` (no TTL).\n\n### Priority Aging\n\nAutomatically boosts priority of old waiting jobs to prevent starvation:\n\n```typescript\nconst app = new Bunqueue('tasks', {\n  embedded: true,\n  processor: async (job) =\u003e ({ done: true }),\n  priorityAging: {\n    interval: 60000,\n    minAge: 300000,\n    boost: 2,\n    maxPriority: 100,\n    maxScan: 200,\n  },\n});\n```\n\nA job with priority 1, after 5 min: 3, after 10 min: 5, … capped at 100.\n\n### Deduplication\n\nPrevent duplicate jobs. Jobs with the same name + data get the same dedup ID:\n\n```typescript\nconst app = new Bunqueue('webhooks', {\n  embedded: true,\n  processor: async (job) =\u003e processWebhook(job.data),\n  deduplication: {\n    ttl: 60000,\n    extend: false,\n    replace: false,\n  },\n});\n\nawait app.add('hook', { event: 'user.created', userId: '123' });\nawait app.add('hook', { event: 'user.created', userId: '123' }); // deduplicated!\nawait app.add('hook', { event: 'user.updated', userId: '123' }); // different data → new job\n```\n\nOverride per-job: `await app.add('task', data, { deduplication: { id: 'my-id', ttl: 5000 } })`.\n\n### Debouncing\n\nCoalesce rapid same-name jobs. Only the last one in the TTL window gets processed:\n\n```typescript\nconst app = new Bunqueue('search', {\n  embedded: true,\n  processor: async (job) =\u003e executeSearch(job.data.query),\n  debounce: { ttl: 500 },\n});\n\nawait app.add('search', { query: 'h' });\nawait app.add('search', { query: 'he' });\nawait app.add('search', { query: 'hello' });  // only this one processes\n```\n\n### Rate Limiting\n\n```typescript\nconst app = new Bunqueue('api', {\n  embedded: true,\n  processor: async (job) =\u003e callExternalAPI(job.data),\n  rateLimit: { max: 100, duration: 1000 },\n});\n\n// Per-group rate limiting (e.g., per customer)\nconst app2 = new Bunqueue('api', {\n  embedded: true,\n  processor: async (job) =\u003e callAPI(job.data),\n  rateLimit: { max: 10, duration: 1000, groupKey: 'customerId' },\n});\n\napp.setGlobalRateLimit(50, 1000);\napp.removeGlobalRateLimit();\n```\n\n### DLQ (Dead Letter Queue)\n\n```typescript\nconst app = new Bunqueue('critical', {\n  embedded: true,\n  processor: async (job) =\u003e riskyOperation(job.data),\n  dlq: {\n    autoRetry: true,\n    autoRetryInterval: 3600000,\n    maxAutoRetries: 3,\n    maxAge: 604800000,\n    maxEntries: 10000,\n  },\n});\n\napp.getDlq();                        // all entries\napp.getDlqStats();                   // { total, byReason, ... }\napp.getDlq({ reason: 'timeout' });   // filter by reason\napp.retryDlq();                      // retry all\napp.purgeDlq();                      // clear all\n```\n\nFailure reasons: `explicit_fail`, `max_attempts_exceeded`, `timeout`, `stalled`, `ttl_expired`, `worker_lost`.\n\n### Cron Jobs\n\n```typescript\nawait app.cron('daily-report', '0 9 * * *', { type: 'report' });\nawait app.cron('eu-digest', '0 8 * * 1', { type: 'weekly' }, { timezone: 'Europe/Rome' });\nawait app.every('healthcheck', 30000, { type: 'ping' });\n\nawait app.listCrons();\nawait app.removeCron('healthcheck');\n```\n\n### Events\n\n```typescript\napp.on('completed', (job, result) =\u003e { });\napp.on('failed', (job, error) =\u003e { });\napp.on('active', (job) =\u003e { });\napp.on('progress', (job, progress) =\u003e { });\napp.on('stalled', (jobId, reason) =\u003e { });\napp.on('error', (error) =\u003e { });\napp.on('ready', () =\u003e { });\napp.on('drained', () =\u003e { });\napp.on('closed', () =\u003e { });\n```\n\n### Adding Jobs\n\n```typescript\nawait app.add('task', { key: 'value' });\nawait app.add('urgent', data, { priority: 10, delay: 5000, attempts: 5, durable: true });\nawait app.addBulk([\n  { name: 'email', data: { to: 'alice' } },\n  { name: 'email', data: { to: 'bob' }, opts: { priority: 10 } },\n]);\n```\n\n### Control\n\n```typescript\napp.pause();           // pause queue + worker\napp.resume();          // resume both\nawait app.close();     // graceful shutdown\nawait app.close(true); // force shutdown\n```\n\n### Full Example\n\n```typescript\nimport { Bunqueue, shutdownManager } from 'bunqueue/client';\n\nconst app = new Bunqueue\u003c{ payload: string }\u003e('my-app', {\n  embedded: true,\n  routes: {\n    'process': async (job) =\u003e ({ id: job.data.payload, status: 'done' }),\n    'notify': async (job) =\u003e ({ sent: true }),\n    'alert': async (job) =\u003e ({ alerted: true }),\n  },\n  concurrency: 10,\n  retry: { maxAttempts: 3, delay: 1000, strategy: 'jitter' },\n  circuitBreaker: { threshold: 5, resetTimeout: 30000 },\n  ttl: { defaultTtl: 600000, perName: { 'verify-otp': 60000 } },\n  priorityAging: { interval: 60000, minAge: 300000, boost: 1 },\n  deduplication: { ttl: 5000 },\n  rateLimit: { max: 100, duration: 1000 },\n  dlq: { autoRetry: true, maxAge: 604800000 },\n});\n\napp.use(async (job, next) =\u003e {\n  const start = Date.now();\n  const result = await next();\n  console.log(`${job.name}: ${Date.now() - start}ms`);\n  return result;\n});\n\napp\n  .trigger({ on: 'process', create: 'notify', data: (r) =\u003e ({ payload: r.id }) })\n  .trigger({ on: 'process', event: 'failed', create: 'alert', data: (_, j) =\u003e j.data });\n\nawait app.cron('cleanup', '0 2 * * *', { payload: 'nightly' });\nawait app.add('process', { payload: 'ORD-001' });\n\nprocess.on('SIGINT', async () =\u003e {\n  await app.close();\n  shutdownManager();\n});\n```\n\n[Simple Mode docs →](https://bunqueue.dev/guide/simple-mode/)\n\n---\n\n## Workflow Engine\n\nOrchestrate multi-step business processes with branching, saga compensation, and human-in-the-loop signals. Built on top of bunqueue — no new infrastructure.\n\n```typescript\nimport { Workflow, Engine } from 'bunqueue/workflow';\n\nconst orderFlow = new Workflow('order-pipeline')\n  .step('validate', async (ctx) =\u003e {\n    const { orderId, amount } = ctx.input as { orderId: string; amount: number };\n    if (amount \u003c= 0) throw new Error('Invalid amount');\n    return { orderId };\n  })\n  .step('reserve-stock', async () =\u003e {\n    await inventory.reserve();\n    return { reserved: true };\n  }, {\n    compensate: async () =\u003e await inventory.release(), // Auto-rollback on failure\n  })\n  .step('charge', async () =\u003e {\n    return { txId: await payments.charge() };\n  }, {\n    compensate: async () =\u003e await payments.refund(),\n  })\n  .step('confirm', async (ctx) =\u003e {\n    const { txId } = ctx.steps['charge'] as { txId: string };\n    return { emailSent: true, txId };\n  });\n\nconst engine = new Engine({ embedded: true });\nengine.register(orderFlow);\nawait engine.start('order-pipeline', { orderId: 'ORD-1', amount: 99.99 });\n```\n\n**Features:**\n\n- **Saga pattern** — Compensation handlers run in reverse when a step fails\n- **Branching** — Route to different paths based on runtime conditions\n- **Parallel steps** — Run independent steps concurrently with `.parallel()`\n- **Human-in-the-loop** — `waitFor('event')` pauses execution, `engine.signal()` resumes it\n- **Signal timeout** — `waitFor('event', { timeout })` fails if signal doesn't arrive in time\n- **Step retry** — Automatic retry with exponential backoff and jitter\n- **Nested workflows** — Compose workflows with `.subWorkflow()`, child results passed back\n- **Loops** — `doUntil()` and `doWhile()` for conditional iteration with safety limits\n- **forEach** — Iterate over dynamic item lists with indexed step results (`step:0`, `step:1`, ...)\n- **Map** — Synchronous data transforms between steps with `.map()`\n- **Schema validation** — Validate step input/output with Zod, ArkType, Valibot, or any `.parse()` schema\n- **Subscribe** — `engine.subscribe(id, callback)` to monitor a specific execution's events\n- **Observability** — Typed event emitter with 11 event types (`engine.on/onAny`)\n- **Cleanup \u0026 archival** — `engine.cleanup()` / `engine.archive()` for execution history management\n- **Step timeouts** — Prevent steps from running indefinitely\n- **Context passing** — Each step accesses input and all previous step results\n- **SQLite persistence** — Execution state survives restarts\n\n**vs Competitors:**\n\n| | **bunqueue** | **Temporal** | **Inngest** | **Trigger.dev** |\n|---|---|---|---|---|\n| **Infrastructure** | None (embedded) | PostgreSQL + 7 services | Cloud-only | Redis + PostgreSQL |\n| **Saga compensation** | Built-in | Manual | Manual | Manual |\n| **Human-in-the-loop** | `.waitFor()` | Signals API | `step.waitForEvent()` | Waitpoint tokens |\n| **Self-hosted** | Zero-config | Complex | No | Complex |\n| **Pricing** | Free (MIT) | Free / Cloud $$ | Per-execution | Free tier, then $50/mo+ |\n\n```typescript\n// Branching\nconst flow = new Workflow('tiered')\n  .step('classify', async (ctx) =\u003e {\n    const { amount } = ctx.input as { amount: number };\n    return { tier: amount \u003e 1000 ? 'vip' : 'basic' };\n  })\n  .branch((ctx) =\u003e (ctx.steps['classify'] as { tier: string }).tier)\n  .path('vip', (w) =\u003e w.step('vip-handler', async () =\u003e ({ discount: 20 })))\n  .path('basic', (w) =\u003e w.step('basic-handler', async () =\u003e ({ discount: 0 })))\n  .step('done', async () =\u003e ({ processed: true }));\n\n// Human-in-the-loop\nconst approvalFlow = new Workflow('expense')\n  .step('submit', async (ctx) =\u003e {\n    const { amount } = ctx.input as { amount: number };\n    return { amount };\n  })\n  .waitFor('manager-approval')\n  .step('reimburse', async (ctx) =\u003e {\n    const decision = ctx.signals['manager-approval'] as { approved: boolean };\n    return { status: decision.approved ? 'paid' : 'rejected' };\n  });\n\n// Signal a waiting workflow\nawait engine.signal(run.id, 'manager-approval', { approved: true });\n```\n\n[Workflow Engine docs →](https://bunqueue.dev/guide/workflow/)\n\n---\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003ebunqueue Dashboard\u003c/strong\u003e\u003cbr/\u003e\n  \u003csub\u003eA visual interface for managing queues, jobs, workers and monitoring in real time. Currently in beta.\u003c/sub\u003e\n\u003c/p\u003e\n\nhttps://github.com/user-attachments/assets/e8a8d38e-b4a6-4dc8-8360-876c0f24d116\n\n\u003cp align=\"center\"\u003e\n  \u003csub\u003eWant early access? Reach out at \u003cb\u003eegeominotti@gmail.com\u003c/b\u003e\u003c/sub\u003e\n\u003c/p\u003e\n\n---\n\n## Why bunqueue?\n\n| Library      | Requires    | AI-native |\n| ------------ | ----------- | --------- |\n| BullMQ       | Redis       | No        |\n| Agenda       | MongoDB     | No        |\n| pg-boss      | PostgreSQL  | No        |\n| **bunqueue** | **Nothing** | **Yes**   |\n\n- **MCP server included** — 73 tools, 5 resources, 3 prompts. AI agents get full control out of the box\n- **BullMQ-compatible API** — Same `Queue`, `Worker`, `QueueEvents`\n- **Zero dependencies** — No Redis, no MongoDB\n- **SQLite persistence** — Survives restarts, WAL mode for concurrent access\n- **Up to 286K ops/sec** — [Verified benchmarks](https://bunqueue.dev/guide/benchmarks/)\n\n## Built for AI Agents (MCP Server)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\".github/mcp-flow.svg\" alt=\"HTTP Handler Flow: Cron/Add Job → Queue → Embedded Worker → HTTP API → Job Result\" width=\"700\" /\u003e\n\u003c/p\u003e\n\nbunqueue is the **first job queue with native MCP support**. AI agents get a full-featured scheduler, task queue, and monitoring system — no glue code needed.\n\n**HTTP Handlers** solve a fundamental problem: an AI agent can schedule jobs and manage queues, but it cannot run a persistent worker. When the agent registers an HTTP handler, bunqueue spawns an embedded Worker that continuously pulls jobs and calls your HTTP endpoint. Responses are saved as results. Failed calls retry automatically via DLQ.\n\n**What AI agents can do with bunqueue:**\n\n- **Schedule tasks** — cron jobs, delayed execution, recurring workflows\n- **Manage job pipelines** — push jobs, monitor progress, retry failures\n- **Full pull/ack/fail cycle** — agents can consume and process jobs directly\n- **Monitor everything** — stats, memory, Prometheus metrics, logs, DLQ\n- **Control flow** — pause/resume queues, set rate limits, manage concurrency\n- **73 MCP tools + 5 resources + 3 prompts** — complete control over every feature\n- **HTTP handlers** — register a URL, bunqueue auto-processes jobs via HTTP calls\n\n```bash\n# One command to connect Claude Code\nclaude mcp add bunqueue -- bunx bunqueue-mcp\n```\n\n```json\n// Claude Desktop / Cursor / Windsurf — add to MCP config\n{\n  \"mcpServers\": {\n    \"bunqueue\": {\n      \"command\": \"bunx\",\n      \"args\": [\"bunqueue-mcp\"]\n    }\n  }\n}\n```\n\n**Example agent interactions:**\n\n- *\"Schedule a cleanup job every day at 3 AM\"*\n- *\"Add 500 email jobs to the queue with priority 10\"*\n- *\"Show me all failed jobs and retry them\"*\n- *\"Set rate limit to 50/sec on the api-calls queue\"*\n- *\"What's the memory usage and queue throughput?\"*\n\n**Plugin ecosystem** — bunqueue ships with auto-discovery (`.mcp.json`), a custom Claude Code agent for bunqueue tasks, and installable skills for setup, API reference, and real-world patterns. Drop bunqueue into any project and your AI tools discover it automatically.\n\nSupports **embedded** (local SQLite) and **TCP** (remote server) modes. [Full MCP documentation →](https://bunqueue.dev/guide/mcp/)\n\n## When to use bunqueue\n\n**Great for:**\n\n- **AI agents that need a scheduler** — cron jobs, delayed tasks, retries, all via MCP\n- **Agentic workflows** — agents push jobs, workers process, agents monitor results\n- Single-server deployments\n- Prototypes and MVPs\n- Moderate to high workloads (up to 286K ops/sec)\n- Teams that want to avoid Redis operational overhead\n- Embedded use cases (CLI tools, edge functions, serverless)\n\n**Not ideal for:**\n\n- Multi-region distributed systems requiring HA\n- Workloads that need automatic failover today\n- Systems already running Redis with existing infrastructure\n\n## Why not just use BullMQ?\n\nIf you're already running Redis, BullMQ is great — battle-tested and feature-rich.\n\nbunqueue is for when you **don't want to run Redis**. SQLite with WAL mode handles surprisingly high throughput for single-node deployments (tested up to 286K ops/sec). You get persistence, priorities, delays, retries, cron jobs, and DLQ — without the operational overhead of another service.\n\n## Install\n\n```bash\nbun add bunqueue\n```\n\n\u003e Requires [Bun](https://bun.sh) runtime. Node.js is not supported.\n\n## Two Modes\n\nbunqueue runs in two modes depending on your architecture:\n\n|                  | Embedded                              | Server (TCP)                                 |\n| ---------------- | ------------------------------------- | -------------------------------------------- |\n| **How it works** | Queue runs inside your process        | Standalone server, clients connect via TCP   |\n| **Setup**        | `bun add bunqueue`                    | `docker run` or `bunqueue start`             |\n| **Performance**  | 286K ops/sec                          | 149K ops/sec                                 |\n| **Best for**     | Single-process apps, CLIs, serverless | Multiple workers, separate producer/consumer |\n| **Scaling**      | Same process only                     | Multiple clients across machines             |\n\n### Embedded Mode\n\nEverything runs in your process. No server, no network, no setup.\n\n```typescript\nimport { Queue, Worker } from 'bunqueue/client';\n\nconst queue = new Queue('emails', { embedded: true });\n\nconst worker = new Worker(\n  'emails',\n  async (job) =\u003e {\n    console.log('Processing:', job.data);\n    return { sent: true };\n  },\n  { embedded: true }\n);\n\nawait queue.add('welcome', { to: 'user@example.com' });\n```\n\n### Server Mode (TCP)\n\nRun bunqueue as a standalone server. Multiple workers and producers connect via TCP.\n\n```bash\n# Start with persistent data\ndocker run -d -p 6789:6789 -p 6790:6790 \\\n  -v bunqueue-data:/app/data \\\n  ghcr.io/egeominotti/bunqueue:latest\n```\n\nConnect from your app:\n\n```typescript\nimport { Queue, Worker } from 'bunqueue/client';\n\nconst queue = new Queue('tasks', { connection: { host: 'localhost', port: 6789 } });\n\nconst worker = new Worker(\n  'tasks',\n  async (job) =\u003e {\n    return { done: true };\n  },\n  { connection: { host: 'localhost', port: 6789 } }\n);\n\nawait queue.add('process', { data: 'hello' });\n```\n\n### Simple Mode\n\nOne object. Queue + Worker + Routes + Middleware + Cron. Zero boilerplate.\n\n```typescript\nimport { Bunqueue } from 'bunqueue/client';\n\nconst app = new Bunqueue('notifications', {\n  embedded: true,\n\n  // Route jobs by name\n  routes: {\n    'send-email': async (job) =\u003e {\n      console.log(`Email to ${job.data.to}`);\n      return { sent: true };\n    },\n    'send-sms': async (job) =\u003e {\n      console.log(`SMS to ${job.data.to}`);\n      return { sent: true };\n    },\n  },\n  concurrency: 10,\n});\n\n// Middleware — wraps every job (logging, timing, error recovery)\napp.use(async (job, next) =\u003e {\n  const start = Date.now();\n  const result = await next();\n  console.log(`${job.name} took ${Date.now() - start}ms`);\n  return result;\n});\n\n// Cron — scheduled jobs\nawait app.cron('daily-report', '0 9 * * *', { type: 'summary' });\nawait app.every('healthcheck', 30000, { type: 'ping' });\n\n// Events\napp.on('completed', (job, result) =\u003e console.log(result));\napp.on('failed', (job, err) =\u003e console.error(err));\n\n// Add jobs\nawait app.add('send-email', { to: 'alice@example.com' });\nawait app.add('send-sms', { to: '+1234567890' });\n\n// Graceful shutdown\nawait app.close();\n```\n\nWorks with both embedded and TCP mode. [Simple Mode docs →](https://bunqueue.dev/guide/simple-mode/)\n\n## Performance\n\nSQLite handles surprisingly high throughput for single-node deployments:\n\n| Mode     | Peak Throughput | Use Case            |\n| -------- | --------------- | ------------------- |\n| Embedded | 286K ops/sec    | Same process        |\n| TCP      | 149K ops/sec    | Distributed workers |\n\n\u003e Run `bun run bench` to verify on your hardware. [Full benchmark methodology →](https://bunqueue.dev/guide/benchmarks/)\n\n## Monitoring\n\n```bash\n# Start with Prometheus + Grafana\ndocker compose --profile monitoring up -d\n```\n\n- **Grafana**: http://localhost:3000 (admin/bunqueue)\n- **Prometheus**: http://localhost:9090\n\n## Documentation\n\n**[Read the full documentation →](https://bunqueue.dev/)**\n\n- [Quick Start](https://bunqueue.dev/guide/quickstart/)\n- [Workflow Engine](https://bunqueue.dev/guide/workflow/)\n- [MCP Server (AI Agents)](https://bunqueue.dev/guide/mcp/)\n- [Simple Mode](https://bunqueue.dev/guide/simple-mode/)\n- [Queue API](https://bunqueue.dev/guide/queue/)\n- [Worker API](https://bunqueue.dev/guide/worker/)\n- [Server Mode](https://bunqueue.dev/guide/server/)\n- [Benchmarks](https://bunqueue.dev/guide/benchmarks/)\n- [CLI Reference](https://bunqueue.dev/guide/cli/)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fegeominotti%2Fbunqueue","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fegeominotti%2Fbunqueue","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fegeominotti%2Fbunqueue/lists"}