I was building a notification system for a client last week when I hit a familiar wall. The core API was fast, but sending thousands of welcome emails brought everything to a grinding halt. The user clicked ‘sign up,’ and then they waited. That’s a poor experience. It got me thinking about the silent workhorses of modern applications—the background jobs that handle everything from sending emails to cleaning up data. If you’re building with NestJS and need that kind of reliable, scheduled task processing, I want to show you a powerful combination. Let’s look at how to bring Agenda, a solid job scheduler, into your NestJS project. This setup can turn a slow, blocking operation into a smooth, background process that users never have to wait for.
Why consider Agenda? It’s a job scheduler that uses MongoDB to store its state. This is its biggest strength. Jobs, their schedules, and their results live in your database. If your server restarts, the jobs persist. If you need to scale out by running multiple instances of your app, they can all work from the same queue. You don’t need to introduce another piece of infrastructure like Redis. For many projects, especially those already using MongoDB, this is a much simpler path.
So, how do we make NestJS and Agenda work together? The goal is to create a clean module that we can import anywhere in our application. We’ll wrap Agenda’s functionality in a service that fits neatly into NestJS’s dependency injection system. This means any other service or controller can easily queue a job without worrying about the low-level details.
First, we need to install the core packages. You’ll need agenda and @types/agenda for TypeScript support.
npm install agenda
npm install --save-dev @types/agenda
Now, let’s build the core service. We’ll create an AgendaService that sets up the connection and provides methods to schedule jobs. Notice how we use the OnModuleInit lifecycle hook. This ensures our Agenda instance starts only after the module is ready.
// agenda.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { Agenda } from 'agenda';
@Injectable()
export class AgendaService implements OnModuleInit, OnModuleDestroy {
private agenda: Agenda;
constructor() {
// Connect to your MongoDB instance
this.agenda = new Agenda({
db: { address: process.env.MONGODB_URI },
});
}
async onModuleInit() {
await this.agenda.start();
console.log('Agenda job scheduler started');
}
async onModuleDestroy() {
await this.agenda.stop();
console.log('Agenda job scheduler stopped');
}
// A method to schedule a one-time job in the future
scheduleJob(jobName: string, when: Date, data: any): void {
this.agenda.schedule(when, jobName, data);
}
// A method to run a job every 5 minutes
defineRecurringJob(jobName: string, interval: string): void {
this.agenda.define(jobName, async (job) => {
console.log(`Executing job: ${jobName}`, job.attrs.data);
// Your job logic goes here
});
this.agenda.every(interval, jobName);
}
// Expose the Agenda instance for more complex use cases
getAgendaInstance(): Agenda {
return this.agenda;
}
}
But what does a job actually look like in practice? Let’s define a real one. Imagine you need to send a follow-up email 24 hours after a user signs up. Instead of making the user wait, your API can simply drop a job into Agenda. Here’s how you might define that email job processor.
// welcome-email.job.ts
import { Injectable } from '@nestjs/common';
import { AgendaService } from './agenda.service';
@Injectable()
export class WelcomeEmailJob {
constructor(private agendaService: AgendaService) {
this.define();
}
define() {
const agenda = this.agendaService.getAgendaInstance();
agenda.define('send_welcome_followup', async (job) => {
const { userId, email } = job.attrs.data;
console.log(`Sending follow-up email to ${email} for user ${userId}`);
// In reality, you would call your email service here
// await this.emailService.sendFollowUp(userId);
});
}
// This method is called from your user service after signup
scheduleFollowUp(userId: string, userEmail: string) {
const in24Hours = new Date(Date.now() + 24 * 60 * 60 * 1000);
this.agendaService.scheduleJob('send_welcome_followup', in24Hours, {
userId,
email: userEmail,
});
}
}
Now, you can inject WelcomeEmailJob into your UserService. When a user signs up, you call scheduleFollowUp. It returns instantly, and the email sends a day later, all without the user noticing. Have you considered what happens if the email service is temporarily down when the job runs? Agenda has a built-in answer for that.
Agenda handles failure gracefully. You can configure jobs to retry automatically. This is crucial for operations that depend on external services. You can define the number of retries and the delay between them directly in the job definition. This turns a potential point of failure into a self-healing process.
agenda.define('send_report', { concurrency: 1, priority: 'high' }, async (job) => {
// This job will run with high priority, and only one instance at a time
await generateAndSendReport();
});
Monitoring is straightforward. Since all jobs are documents in MongoDB, you can query their state. You can see which jobs are scheduled, running, or failed. For a simple dashboard, you could even build a small admin panel that lists pending jobs. This transparency is a huge advantage when debugging.
Is this setup right for every scenario? No. If you need to process hundreds of thousands of jobs per minute, a Redis-based system like Bull might be faster. But for most applications—sending emails, generating nightly reports, updating caches—Agenda with NestJS is more than capable. It reduces complexity by leveraging your existing database.
The real benefit is structure. By wrapping Agenda in a NestJS service, you keep your code organized. Job logic is contained in dedicated classes. They can use dependency injection to access your database models, configuration, or other services. This is the NestJS way: building complex features from simple, testable blocks.
I encourage you to start with a single job. Take that slow, blocking function in your code and move it into a scheduled task. You’ll be surprised how much more responsive your application feels. Your users will thank you for the snappy interface, and you’ll thank yourself for the cleaner architecture.
If you found this walk-through helpful, please share it with another developer who might be wrestling with background jobs. Have you tried a different job queue with NestJS? What was your experience? Let me know in the comments below—I read every one.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva