Sending Emails
Email sending is the canonical background job use case: you want HTTP responses to return fast, not wait on an SMTP server. Here's how to do it well with Sidequest.
Basic email job
// jobs/SendEmailJob.ts
import { Job } from "sidequest";
import { sendMail } from "../lib/mailer.js"; // your email lib (Nodemailer, Resend, etc.)
export class SendEmailJob extends Job {
async run(to: string, subject: string, body: string) {
await sendMail({ to, subject, html: body });
return { sent: true, to };
}
}// Enqueue from your route handler
import { Sidequest } from "sidequest";
import { SendEmailJob } from "./jobs/SendEmailJob.js";
// In your POST /register handler:
await db.users.create({ email, name }); // create the user first
await Sidequest.build(SendEmailJob)
.queue("emails")
.maxAttempts(3)
.retryDelay(5000) // wait 5s before first retry
.enqueue(email, "Welcome to Acme!", "<h1>Thanks for signing up</h1>");The job retries up to 3 times if the SMTP server is unreachable. The HTTP handler returns immediately after enqueue.
Prevent duplicate welcome emails
If your route can be called twice (retried client, duplicate form submission), you might send the same welcome email twice. Use unique() to prevent that:
await Sidequest.build(SendEmailJob)
.queue("emails")
.maxAttempts(3)
.unique({ key: `welcome:${userId}`, ttl: 24 * 60 * 60 * 1000 }) // deduplicate for 24h
.enqueue(email, "Welcome!", body);If the same key is enqueued again within the TTL window, the second call is silently ignored.
Scheduled digest email
// jobs/WeeklyDigestJob.ts
import { Job } from "sidequest";
export class WeeklyDigestJob extends Job {
async run() {
const users = await db.users.findAll({ where: { notifications: true } });
for (const user of users) {
const digest = await buildDigest(user);
await sendMail({ to: user.email, subject: "Your weekly digest", html: digest });
}
}
}// worker.ts — register the schedule at startup
await Sidequest.start({ ... });
Sidequest.build(WeeklyDigestJob)
.queue("emails")
.unique({ period: "week" }) // only one run per week across all instances
.schedule("0 9 * * 1"); // every Monday at 9amThe unique({ period: "week" }) ensures that even with multiple worker instances, only one digest job is created per week.
Retry with exponential backoff
For transient SMTP failures, exponential backoff avoids hammering a struggling mail server:
await Sidequest.build(SendEmailJob)
.queue("emails")
.maxAttempts(5)
.retryDelay(10000) // 10s base delay
.backoffStrategy("exponential") // 10s, 20s, 40s, 80s, 160s
.enqueue(to, subject, body);Handling permanent failures
If an email address is invalid (hard bounce), retrying is pointless. Return this.fail() from inside run() to mark the job as permanently failed without consuming remaining attempts:
async run(to: string, subject: string, body: string) {
try {
await sendMail({ to, subject, html: body });
} catch (error) {
if (isHardBounce(error)) {
return this.fail(`Hard bounce for ${to}: ${error.message}`);
}
throw error; // let Sidequest retry transient errors
}
}