Background Jobs
Run asynchronous tasks with separate workers since Thunder is serverless-first.
Background jobs are not supported inside a Thunder app itself, because Thunder is designed for serverless-first deployment. Serverless environments don't keep persistent connections between requests or run long-lived processes.
For asynchronous work, run it outside the request lifecycle using one of the patterns below.
Why No Built-in Background Jobs?
Thunder is serverless-first, which means:
- No persistent connections between requests
- No long-running background processes
- Functions scale independently
- Each request is stateless and isolated
For async tasks, use external services instead of built-in job queues.
Option 1: Separate Worker Server (Recommended)
Create a separate worker application that runs on stateful hosting (a VPS, Docker container, Railway, Kubernetes, etc.) and polls your database:
import { mongodb } from "@/database.ts";
import { emailQueueModel } from "@/schemas/emailQueue.ts";
async function processEmailQueue() {
while (true) {
const email = await emailQueueModel.findOne({ status: "pending" });
if (!email) {
await new Promise((resolve) => setTimeout(resolve, 5000)); // wait 5s
continue;
}
try {
await sendEmail(email);
await emailQueueModel.updateOne(
{ _id: email._id },
{ $set: { status: "sent", sentAt: new Date() } }
);
} catch (error) {
await emailQueueModel.updateOne(
{ _id: email._id },
{ $set: { status: "failed", error: error.message } }
);
}
}
}
await processEmailQueue();Option 2: Third-Party Queue Service
Use a managed job queue such as BullMQ. Enqueue jobs from your Thunder app, and process them in a separate worker:
import Queue from "bullmq";
const emailQueue = new Queue("emails", {
connection: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
},
});
export default new Router("/api", function email(router) {
router.post("/send", function sendEmail() {
const $body = z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
});
return {
shape: () => ({ body: $body }),
handler: async (req) => {
const data = $body.parse(await bodyAsJson(req));
// Returns immediately; the worker handles delivery.
await emailQueue.add("send", data, {
attempts: 3,
backoff: { type: "exponential", delay: 2000 },
});
return Response.ok();
},
};
});
});import Queue from "bullmq";
const emailQueue = new Queue("emails", {
connection: { host: process.env.REDIS_HOST },
});
emailQueue.process(async (job) => {
await sendEmail(job.data);
});Option 3: Queue in the Database
The simplest approach uses MongoDB itself as a queue. Enqueue from a route handler, then process with a separate worker (like Option 1):
// In a route handler - enqueue
await emailQueueModel.insertOne(
$emailQueue.strictParse({
type: "send-email",
data: { to, subject, body },
status: "pending",
createdAt: new Date(),
})
);