Skip to content
Get started
Get started

bunqueue with Hono and Elysia: Bun Framework Integration Guide

blog · integrations

Hono and Elysia, queued cleanly.

Hono and Elysia are the most popular web frameworks for Bun, and bunqueue is built for the same ecosystem. Queue jobs from HTTP handlers, wire up middleware, and shut down gracefully.

import { Hono } from 'hono';
import { Queue, Worker } from 'bunqueue/client';
// Create queue and worker
const emailQueue = new Queue<{ to: string; subject: string }>('emails', {
embedded: true,
});
const worker = new Worker('emails', async (job) => {
await sendEmail(job.data.to, job.data.subject);
return { sent: true };
}, { embedded: true, concurrency: 5 });
// Create Hono app
const app = new Hono();
app.post('/api/send-email', async (c) => {
const { to, subject } = await c.req.json();
const job = await emailQueue.add('send', { to, subject });
return c.json({ jobId: job.id, status: 'queued' });
});
app.get('/api/job/:id', async (c) => {
const job = await emailQueue.getJob(c.req.param('id'));
if (!job) return c.json({ error: 'Not found' }, 404);
return c.json({
id: job.id,
state: await job.getState(),
progress: job.progress,
data: job.data,
});
});
export default app;

Create a reusable middleware that injects the queue into the context:

import { createMiddleware } from 'hono/factory';
import { Queue } from 'bunqueue/client';
// Queue factory middleware
export function withQueue<T>(name: string) {
const queue = new Queue<T>(name, { embedded: true });
return createMiddleware(async (c, next) => {
c.set('queue', queue);
await next();
});
}
// Usage
const app = new Hono();
app.use('/api/orders/*', withQueue<OrderData>('orders'));
app.post('/api/orders', async (c) => {
const queue = c.get('queue') as Queue<OrderData>;
const order = await c.req.json();
const job = await queue.add('process', order, {
priority: order.express ? 10 : 1,
attempts: 3,
});
return c.json({ orderId: job.id });
});
import { Elysia } from 'elysia';
import { Queue, Worker } from 'bunqueue/client';
const taskQueue = new Queue<{ type: string; payload: unknown }>('tasks', {
embedded: true,
});
const worker = new Worker('tasks', async (job) => {
switch (job.data.type) {
case 'resize-image':
return await resizeImage(job.data.payload);
case 'generate-pdf':
return await generatePdf(job.data.payload);
default:
throw new Error(`Unknown task type: ${job.data.type}`);
}
}, { embedded: true, concurrency: 3 });
const app = new Elysia()
.post('/tasks', async ({ body }) => {
const job = await taskQueue.add(body.type, body);
return { jobId: job.id, status: 'queued' };
})
.get('/tasks/:id', async ({ params }) => {
const job = await taskQueue.getJob(params.id);
if (!job) return { error: 'Not found' };
return {
id: job.id,
state: await job.getState(),
progress: job.progress,
};
})
.get('/tasks/counts', async () => {
return await taskQueue.getJobCountsAsync();
})
.listen(3000);

Encapsulate the queue as an Elysia plugin:

import { Elysia } from 'elysia';
import { Queue, Worker } from 'bunqueue/client';
export function queuePlugin<T>(name: string) {
const queue = new Queue<T>(name, { embedded: true });
return new Elysia({ name: `queue-${name}` })
.decorate('queue', queue)
.get(`/queues/${name}/counts`, async ({ queue }) => {
return await queue.getJobCountsAsync();
})
.get(`/queues/${name}/health`, async ({ queue }) => {
const counts = await queue.getJobCountsAsync();
return {
name,
...counts,
healthy: counts.active < 1000,
};
});
}
// Usage
const app = new Elysia()
.use(queuePlugin<EmailData>('emails'))
.use(queuePlugin<OrderData>('orders'))
.listen(3000);

Both frameworks need proper shutdown handling to ensure jobs are completed:

const queue = new Queue('tasks', { embedded: true });
const worker = new Worker('tasks', processor, {
embedded: true,
concurrency: 5,
});
// Handle shutdown signals
async function gracefulShutdown() {
console.log('Shutting down...');
// 1. Stop accepting new HTTP requests
server.stop();
// 2. Close worker (finishes active jobs)
await worker.close();
// 3. Disconnect queue (flushes buffers)
await queue.disconnect();
process.exit(0);
}
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

If your web app and workers run as separate processes, use TCP mode:

// Web server (process 1)
const queue = new Queue('tasks', {
connection: { host: 'bunqueue-server', port: 6789 },
});
// Worker (process 2)
const worker = new Worker('tasks', processor, {
connection: { host: 'bunqueue-server', port: 6789 },
concurrency: 10,
});

This separates concerns: the web server only adds jobs, and dedicated worker processes handle processing. bunqueue’s connection pooling and auto-batching make this efficient even under high load.

Organize related queues with QueueGroup:

import { QueueGroup } from 'bunqueue/client';
const billing = new QueueGroup('billing');
const invoices = billing.getQueue<InvoiceData>('invoices', { embedded: true });
const payments = billing.getQueue<PaymentData>('payments', { embedded: true });
const refunds = billing.getQueue<RefundData>('refunds', { embedded: true });
// Queue names are prefixed: "billing:invoices", "billing:payments", etc.
// Pause all billing queues at once (e.g., during maintenance).
// Group-wide control (pauseAll/resumeAll/drainAll) operates on the
// embedded engine, so it applies to embedded-mode queues.
billing.pauseAll();
billing.resumeAll();