Next.js Background Jobs: Inngest vs Trigger.dev vs Vercel Cron
Choosing the right background job solution for your Next.js application can make or break your MVP's performance and user experience. Whether you're processing payments, sending emails, or handling data imports, you need a reliable way to handle tasks that shouldn't block your users.
In this guide, we'll compare three popular solutions for Next.js background jobs: Inngest, Trigger.dev, and Vercel Cron. Each has distinct advantages depending on your use case, budget, and technical requirements. By the end, you'll know exactly which solution fits your project's needs.
What Makes a Good Background Job Solution
Before diving into the comparison, let's establish what matters most for Next.js applications. Your background job system needs to handle failures gracefully, scale with your traffic, and integrate smoothly with your existing stack.
Reliability is paramount. Jobs must retry automatically when they fail, and you need visibility into what's happening. For MVP development, you also want something that works out of the box without extensive configuration.
Cost predictability matters too, especially for early-stage projects. Some solutions charge per job execution, others per time period. Understanding these models upfront prevents billing surprises as you scale.
Prerequisites
Before implementing any of these solutions, ensure you have:
- A Next.js 13+ application with App Router
- Basic understanding of API routes and server actions
- A database for job persistence (if using Vercel Cron)
- Node.js 18+ for compatibility with all three platforms
Inngest: Event-Driven Background Jobs
Inngest takes an event-driven approach to background jobs. Instead of directly queuing jobs, you trigger events that can spawn multiple functions. This pattern works exceptionally well for complex workflows where one action triggers multiple background processes.
Setting Up Inngest
First, install the Inngest SDK and create your first function:
npm install inngest
Create an Inngest client and define your first function:
// lib/inngest.js
import { Inngest } from 'inngest';
export const inngest = new Inngest({ id: 'my-app' });
export const processPayment = inngest.createFunction(
{ id: 'process-payment' },
{ event: 'payment.created' },
async ({ event, step }) => {
const { paymentId } = event.data;
// Step 1: Validate payment
const payment = await step.run('validate-payment', async () => {
return validatePaymentInDatabase(paymentId);
});
// Step 2: Send confirmation email
await step.run('send-email', async () => {
return sendConfirmationEmail(payment.customerEmail);
});
// Step 3: Update inventory
await step.run('update-inventory', async () => {
return decrementInventory(payment.items);
});
}
);
The step-based approach means if your email service fails, Inngest will retry just that step, not the entire function. This granular retry logic is incredibly powerful for complex workflows.
Create an API route to serve your Inngest functions:
// app/api/inngest/route.js
import { serve } from 'inngest/next';
import { inngest } from '@/lib/inngest';
import { processPayment } from '@/lib/functions';
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [processPayment]
});
Inngest Pricing and Use Cases
Inngest offers a generous free tier with 1,000 function runs per month. Paid plans start at $20/month for 10,000 runs. The pricing scales predictably, making it suitable for MVPs that might see sudden traffic spikes.
Inngest excels when you have complex, multi-step workflows. If processing a single user action requires updating multiple systems, sending notifications, and handling potential rollbacks, Inngest's step functions are ideal. The built-in observability and retry logic save significant development time.
Trigger.dev: Developer-First Background Jobs
Trigger.dev focuses on developer experience with excellent local development tools and comprehensive logging. It's particularly strong for jobs that integrate with external APIs or require complex scheduling.
Setting Up Trigger.dev
Install the Trigger.dev CLI and SDK:
npm install @trigger.dev/sdk
npx @trigger.dev/cli@latest init
Create your first job:
// jobs/process-user-onboarding.js
import { client } from '@/trigger';
client.defineJob({
id: 'process-user-onboarding',
name: 'Process User Onboarding',
version: '0.0.1',
trigger: {
name: 'user.created',
},
run: async (payload, io, ctx) => {
// Create user profile
const user = await io.runTask('create-profile', async () => {
return createUserProfile(payload.userId);
});
// Send welcome email sequence
await io.runTask('send-welcome-series', async () => {
return scheduleWelcomeEmails(user.email);
});
// Set up analytics tracking
await io.runTask('setup-analytics', async () => {
return initializeUserAnalytics(user.id);
});
return { success: true, userId: user.id };
},
});
Trigger.dev's io object provides excellent logging and makes debugging much easier than traditional queue systems. Every task execution is logged with timing information and payload data.
Trigger.dev Development Workflow
The local development experience sets Trigger.dev apart. Run the development server:
npx @trigger.dev/cli@latest dev
This creates a tunnel to your local environment, allowing you to test jobs without deploying. You can trigger jobs from the web dashboard and see real-time logs in your terminal.
For production deployment, Trigger.dev handles the infrastructure. Your jobs run on their servers, not your Vercel functions, which means no cold start delays or timeout limits.
Trigger.dev Pricing and Use Cases
Trigger.dev offers 1,000 job runs per month on the free tier. Paid plans start at $20/month for 10,000 runs. They also offer usage-based pricing for high-volume applications.
Trigger.dev shines for jobs that need extensive debugging and monitoring. If you're integrating with multiple external APIs or building complex data pipelines, the logging and observability features are invaluable. The local development tools make iteration much faster than other solutions.
Vercel Cron: Simple Scheduled Tasks
Vercel Cron is the simplest option, perfect for periodic tasks that don't require complex orchestration. It's built into Vercel's platform and requires minimal setup.
Setting Up Vercel Cron
Create a vercel.json file in your project root:
{
"crons": [
{
"path": "/api/cron/daily-reports",
"schedule": "0 9 * * *"
},
{
"path": "/api/cron/cleanup-temp-files",
"schedule": "0 */6 * * *"
}
]
}
Create the corresponding API routes:
// app/api/cron/daily-reports/route.js
import { NextResponse } from 'next/server';
export async function GET(request) {
// Verify the request is from Vercel Cron
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new NextResponse('Unauthorized', { status: 401 });
}
try {
// Generate and send daily reports
const reports = await generateDailyReports();
await sendReportsToStakeholders(reports);
return NextResponse.json({ success: true, reportsCount: reports.length });
} catch (error) {
console.error('Daily report generation failed:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
Handling Job Persistence with Vercel Cron
Since Vercel Cron doesn't include job queuing, you'll need to implement persistence yourself if jobs might take longer than the function timeout:
// lib/job-queue.js
export async function queueLongRunningJob(jobType, payload) {
await db.job.create({
data: {
type: jobType,
payload: JSON.stringify(payload),
status: 'pending',
createdAt: new Date()
}
});
}
export async function processJobQueue() {
const pendingJobs = await db.job.findMany({
where: { status: 'pending' },
take: 10,
orderBy: { createdAt: 'asc' }
});
for (const job of pendingJobs) {
try {
await processJob(job.type, JSON.parse(job.payload));
await db.job.update({
where: { id: job.id },
data: { status: 'completed', completedAt: new Date() }
});
} catch (error) {
await db.job.update({
where: { id: job.id },
data: {
status: 'failed',
error: error.message,
failedAt: new Date()
}
});
}
}
}
Vercel Cron Limitations and Use Cases
Vercel Cron is free for all Vercel users but has significant limitations. Functions are subject to Vercel's timeout limits (10 seconds on Hobby, 5 minutes on Pro). There's no built-in retry logic or job persistence.
Vercel Cron works best for simple, scheduled maintenance tasks like cleanup jobs, report generation, or health checks. It's perfect for MVPs that need basic scheduling without the complexity of external services.
Common Implementation Mistakes
The biggest mistake developers make is choosing a solution based on initial simplicity rather than long-term needs. Vercel Cron seems easiest at first, but you'll quickly hit limitations if your jobs become more complex.
Another common error is not implementing proper error handling and monitoring. Even with Inngest or Trigger.dev's built-in features, you should add application-level logging and alerting for critical jobs.
Don't forget about job idempotency. All three solutions may retry failed jobs, so ensure your functions can run multiple times safely without causing data corruption.
Making the Right Choice for Your MVP
For most MVPs, start with your expected job complexity and volume. If you're building a simple SaaS with basic email notifications and daily reports, Vercel Cron might suffice initially.
Choose Inngest if you have event-driven workflows where one user action triggers multiple background processes. The step-based retry logic and event sourcing approach scale well as your application grows.
Pick Trigger.dev if you need extensive debugging capabilities or plan to integrate with many external services. The development tools and comprehensive logging save significant debugging time.
Consider your MVP tech stack selection criteria when making this decision. Sometimes the best technical solution isn't the right choice for your timeline and budget constraints.
Next Steps
After implementing your chosen background job solution, focus on monitoring and observability. Set up alerts for failed jobs and track execution times to identify performance bottlenecks early.
Consider implementing a job dashboard where you can manually trigger jobs and view execution history. This becomes invaluable during development and troubleshooting.
As your application scales, you might need to combine solutions. Many successful applications use Vercel Cron for simple scheduled tasks and Inngest or Trigger.dev for complex workflows. The key is starting simple and evolving your architecture as requirements become clearer.