A Familiar Scene: Users Waiting and 504 Gateway Timeouts
If you’re using Next.js, you’ve likely encountered this: a user clicks “Sign Up,” and the server starts processing a chain of tasks—from sending emails to creating Stripe accounts and syncing with HubSpot. The loading spinner spins indefinitely. On Vercel, if this process exceeds 10 seconds (Hobby plan) or 60 seconds (Pro plan), the system immediately returns a 504 error.
In a real-world project with over 5,000 daily sign-ups, our team struggled with Promise.all(). If just one third-party API like Stripe responded slowly, the entire request would fail. At that point, tracing whether an email was sent or where the database update left off was a total nightmare.
Why Redis and BullMQ are No Longer “Gold” in the Serverless Era
BullMQ and Redis are a power duo in the traditional Node.js ecosystem. However, when brought into environments like Vercel or AWS Lambda, they reveal three critical weaknesses:
- Bulky Infrastructure: You have to manage a Redis cluster yourself. Maintaining connection pooling between thousands of Serverless functions and Redis often leads to connection exhaustion.
- Asymmetric Structure: BullMQ requires a Worker running 24/7 to listen for jobs. In contrast, Serverless functions only live for a few seconds before disappearing, leaving no room for these Workers to exist.
- Hidden Costs: Managed Redis services like Upstash are great, but as you scale, the cost per request and storage can start to strain the project budget.
Common Alternatives (And Why They Aren’t Good Enough)
Before discovering Inngest, I tried a few quick fixes:
Using setTimeout is the fastest but also the worst way. When a serverless function returns a response, the execution environment is frozen immediately, causing background tasks to be canceled mid-process. Another option is AWS SQS. However, configuring IAM Policies and dozens of technical parameters significantly slows down development speed.
Inngest: A Different Approach to Background Jobs
Inngest doesn’t force you to manage queues. It operates on an Event-driven mechanism. When an event occurs, you simply send a signal (Event) to Inngest Cloud. Inngest then acts as the orchestrator, calling back (via HTTP POST) to an endpoint in your Next.js application to execute the logic.
The most valuable feature is the ability to write complex Workflows using pure TypeScript code. You can tell the system: “Send the email now, then wait exactly 3 days and check if the user has paid.” All of this is encapsulated in a few lines of code without worrying about infrastructure.
Real-world Implementation: Optimized User Signup Flow
Let’s set up Inngest for a real-world Next.js App Router project.
Step 1: Install the library
npm install inngest
Step 2: Initialize the Inngest Client
Create the file src/inngest/client.ts. This is the single entry point for sending events.
import { Inngest } from "inngest";
export const inngest = new Inngest({ id: "my-app-v1" });
Step 3: Define the Background Workflow
In src/inngest/functions.ts, we define the logic. Notice how step.sleep completely replaces complex timer functions.
import { inngest } from "./client";
export const processSignup = inngest.createFunction(
{ id: "process-signup-flow" },
{ event: "app/user.signup" },
async ({ event, step }) => {
// Send email immediately
await step.run("send-welcome-email", async () => {
return { status: "success", email: event.data.email };
});
// Wait 24 hours before sending the survey
await step.sleep("wait-for-survey", "24h");
await step.run("send-survey", async () => {
console.log("Sending survey to:", event.data.email);
});
}
);
Step 4: Set up the Route Handler
Inngest needs a “gateway” to communicate with your application. Create the file src/app/api/inngest/route.ts:
import { serve } from "inngest/next";
import { inngest } from "@/inngest/client";
import { processSignup } from "@/inngest/functions";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [processSignup],
});
Resilience and Cron Jobs
One of the biggest concerns for developers is a third-party API going down. With Inngest, if a step.run fails, the system automatically performs Exponential Backoff (retrying with increasing time intervals). You don’t need to write any additional retry logic.
If you need to run periodic tasks like cleaning the database at 2 AM, Inngest supports a very clean Cron syntax:
export const weeklyCleanup = inngest.createFunction(
{ id: "weekly-cleanup" },
{ cron: "0 2 * * 1" }, // 2 AM every Monday
async ({ step }) => {
await step.run("delete-old-logs", async () => {
// Cleanup logic here
});
}
);
Conclusion from Real-world Experience
After switching from a self-built system to Inngest, our team’s development time for background job-related features dropped from 3 days to about 4 hours. Debugging also became more intuitive thanks to the Dashboard, which tracks the status of each step in real-time.
A tip for fellow devs: When running locally, always keep the Dev Server running with the command npx inngest-cli@latest dev. It perfectly simulates the Cloud environment so you can test workflows without spending a dime. If you want clean code, a fast app, and don’t want the headache of Redis, Inngest is the missing piece for Next.js.

