Cảnh tượng quen thuộc: User chờ đợi và lỗi 504 Gateway Timeout
Nếu bạn đang dùng Next.js, chắc hẳn đã từng gặp tình huống này: Người dùng nhấn “Đăng ký”, server bắt đầu xử lý một chuỗi tác vụ từ gửi email, tạo tài khoản Stripe đến đồng bộ HubSpot. Vòng xoay loading cứ thế quay vô tận. Trên Vercel, nếu quá trình này vượt quá 10 giây (gói Hobby) hoặc 60 giây (gói Pro), hệ thống sẽ trả về lỗi 504 ngay lập tức.
Trong một dự án thực tế với hơn 5.000 user đăng ký mỗi ngày, team mình từng khốn đốn vì Promise.all(). Chỉ cần một API bên thứ ba như Stripe phản hồi chậm, toàn bộ request sẽ thất bại. Lúc đó, việc truy vết xem email đã gửi chưa hay database đã cập nhật tới đâu là một cơn ác mộng thực sự.
Tại sao Redis và BullMQ không còn là “vàng” trong kỷ nguyên Serverless?
BullMQ và Redis là bộ đôi quyền lực trong hệ sinh thái Node.js truyền thống. Tuy nhiên, khi đưa vào các môi trường như Vercel hay AWS Lambda, chúng bộc lộ ba điểm yếu chí mạng:
- Hạ tầng cồng kềnh: Bạn phải tự quản lý một cụm Redis. Việc duy trì kết nối (connection pooling) giữa hàng nghìn hàm Serverless và Redis thường xuyên gây ra tình trạng tràn kết nối.
- Cấu trúc bất đối xứng: BullMQ cần một Worker chạy liên tục 24/7 để nghe job. Ngược lại, Serverless function chỉ sống trong vài giây rồi biến mất, không có chỗ cho các Worker này tồn tại.
- Chi phí ẩn: Các dịch vụ Managed Redis như Upstash rất tốt, nhưng khi scale lớn, chi phí cho mỗi request và lưu trữ sẽ bắt đầu làm khó ngân sách dự án.
Những lựa chọn thay thế thường gặp (và tại sao chúng chưa đủ tốt)
Trước khi đến với Inngest, mình đã thử qua vài phương án chữa cháy:
Dùng setTimeout là cách nhanh nhất nhưng cũng tệ nhất. Khi serverless function trả về response, môi trường thực thi sẽ bị đóng băng ngay lập tức, khiến các tác vụ ngầm bị hủy giữa chừng. Một lựa chọn khác là AWS SQS. Tuy nhiên, việc cấu hình IAM Policy và hàng tá thông số kỹ thuật khiến tốc độ phát triển dự án bị kéo chậm đáng kể.
Inngest: Tư duy khác biệt về Background Jobs
Inngest không bắt bạn quản lý hàng đợi. Nó hoạt động theo cơ chế Event-driven. Khi một sự kiện xảy ra, bạn chỉ cần bắn một tín hiệu (Event) lên Inngest Cloud. Sau đó, Inngest sẽ đóng vai trò là bên điều phối, gọi ngược lại (via HTTP POST) vào endpoint trong ứng dụng Next.js của bạn để thực thi logic.
Điểm đáng giá nhất là khả năng viết Workflow phức tạp bằng code TypeScript thuần túy. Bạn có thể yêu cầu hệ thống: “Gửi mail ngay, sau đó đợi đúng 3 ngày rồi kiểm tra xem user đã thanh toán chưa”. Tất cả chỉ gói gọn trong vài dòng code mà không cần quan tâm đến hạ tầng.
Triển khai thực tế: Luồng đăng ký người dùng tối ưu
Hãy cùng bắt tay vào cài đặt Inngest cho một dự án Next.js App Router thực tế.
Bước 1: Cài đặt thư viện
npm install inngest
Bước 2: Khởi tạo Inngest Client
Tạo file src/inngest/client.ts. Đây là đầu mối duy nhất để gửi event.
import { Inngest } from "inngest";
export const inngest = new Inngest({ id: "my-app-v1" });
Bước 3: Định nghĩa Workflow xử lý ngầm
Tại src/inngest/functions.ts, chúng ta định nghĩa logic. Hãy để ý cách step.sleep thay thế hoàn toàn cho các hàm hẹn giờ phức tạp.
import { inngest } from "./client";
export const processSignup = inngest.createFunction(
{ id: "process-signup-flow" },
{ event: "app/user.signup" },
async ({ event, step }) => {
// Gửi email ngay lập tức
await step.run("send-welcome-email", async () => {
return { status: "success", email: event.data.email };
});
// Chờ 24 giờ sau để gửi khảo sát
await step.sleep("wait-for-survey", "24h");
await step.run("send-survey", async () => {
console.log("Sending survey to:", event.data.email);
});
}
);
Bước 4: Thiết lập Route Handler
Inngest cần một “cổng chào” để giao tiếp với ứng dụng của bạn. Tạo 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],
});
Khả năng tự hồi phục (Resilience) và Cron Jobs
Một trong những nỗi lo lớn nhất của dev là API bên thứ ba bị sập. Với Inngest, nếu một step.run thất bại, hệ thống tự động thực hiện Exponential Backoff (thử lại với khoảng cách thời gian tăng dần). Bạn không cần viết thêm bất kỳ logic retry nào.
Nếu bạn cần chạy tác vụ định kỳ như dọn dẹp database lúc 2 giờ sáng, Inngest hỗ trợ cú pháp Cron cực kỳ gọn nhẹ:
export const weeklyCleanup = inngest.createFunction(
{ id: "weekly-cleanup" },
{ cron: "0 2 * * 1" }, // 2AM mỗi thứ Hai
async ({ step }) => {
await step.run("delete-old-logs", async () => {
// Logic dọn dẹp ở đây
});
}
);
Kết luận từ trải nghiệm thực tế
Sau khi chuyển đổi từ hệ thống tự build sang Inngest, thời gian phát triển các tính năng liên quan đến background job của team mình giảm từ 3 ngày xuống còn khoảng 4 tiếng. Việc debug cũng trở nên trực quan hơn nhờ Dashboard theo dõi trạng thái của từng step theo thời gian thực.
Lời khuyên cho anh em: Khi chạy local, hãy luôn bật Dev Server bằng lệnh npx inngest-cli@latest dev. Nó sẽ giả lập môi trường Cloud hoàn hảo để bạn test workflow mà không tốn một đồng phí nào. Nếu bạn muốn code sạch, app nhanh và không muốn đau đầu vì Redis, Inngest chính là mảnh ghép còn thiếu cho Next.js.

