Inngest Background Jobs
Implemente jobs em background robustos com event-driven functions, scheduling e monitoramento
Por que isso é importante
Jobs em background são fundamentais para SaaS escaláveis.Inngest oferece event-driven functions com retries automáticos, scheduling avançado e monitoramento em tempo real. Substitui Redis/Bull/Sidekiq com uma solução serverless robusta.
📚 Aula Educacional
Esta aula apresenta conceitos e implementações do Inngest como exemplos educacionais. Você aprenderá arquitetura, padrões e best practices sem modificar o projeto atual. Use este conhecimento para implementar jobs em seus próprios projetos SaaS.
🎯 O que São Background Jobs?
Problemas que Resolvem:
- • Tarefas longas bloqueiam UI
- • Emails demoram para enviar
- • Processamento de dados pesados
- • Integrações externas lentas
- • Timeouts em requisições
Casos de Uso SaaS:
- • Envio de emails transacionais
- • Processamento de webhooks
- • Geração de relatórios
- • Backup de dados
- • Notificações push
🤔 Passo 1: Por que Inngest?
🎯 Comparação com Alternativas
Inngest elimina a complexidade de Redis/Bull, oferecendo jobs serverless com interface visual, retries inteligentes e observabilidade integrada.
❌ Soluções Tradicionais
Redis + Bull/BullMQ:
- • Infraestrutura complexa
- • Gerenciamento de filas manual
- • Monitoramento limitado
- • Scaling manual
- • Debugging difícil
Cron Jobs:
- • Sem retries automáticos
- • Sem observabilidade
- • Scheduling limitado
- • Sem event-driven
✅ Inngest Advantages
Serverless + Event-Driven:
- • Zero infraestrutura
- • Auto-scaling nativo
- • Event-driven architecture
- • Type-safe com TypeScript
- • Dashboard visual completo
Reliability:
- • Retries exponenciais automáticos
- • Dead letter queues
- • Monitoring em tempo real
- • Debugging visual
📦 Passo 2: Setup Inngest
🔧 Configuração Inicial
Inngest funciona com qualquer framework. Para Next.js, criamos functions que são servidas via API routes e registradas no Inngest Cloud.
# Instalar Inngest SDK
npm install inngest
# Instalar dependências opcionais para development
npm install --save-dev @inngest/cli⚙️ Passo 3: Configuração Base
1. Cliente Inngest
import { Inngest } from 'inngest';
// 🎯 Cliente Inngest - ponto central para todos os events
export const inngest = new Inngest({
id: 'my-saas-app',
name: 'My SaaS Application',
// 🔒 Event key para segurança (production)
eventKey: process.env.INNGEST_EVENT_KEY,
// 🎨 Schema de events para type-safety
schemas: {
// Definiremos events específicos aqui
}
});2. Variáveis de Ambiente
# Inngest Configuration
INNGEST_EVENT_KEY=your_event_key_here
INNGEST_SIGNING_KEY=your_signing_key_here
# Para development local
INNGEST_DEV=true3. API Route Handler
import { serve } from 'inngest/next';
import { inngest } from '@/inngest/client';
// 🎯 Import todas as functions aqui
import {
sendWelcomeEmail,
processStripeWebhook,
generateMonthlyReport
} from '@/inngest/functions';
// 🚀 Serve Inngest functions via Next.js API route
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [
sendWelcomeEmail,
processStripeWebhook,
generateMonthlyReport,
// Adicione novas functions aqui
],
// 🔒 Configurações de segurança
signingKey: process.env.INNGEST_SIGNING_KEY,
// 🛠️ Development mode
isDev: process.env.NODE_ENV === 'development',
});🚀 Passo 4: Primeira Background Function
📧 Exemplo: Welcome Email
Function que envia email de boas-vindas quando usuário se registra. Demonstra event-driven architecture, type-safety e error handling.
import { inngest } from '../client';
import { sendEmail } from '@/lib/email';
// 📧 Function para enviar email de boas-vindas
export const sendWelcomeEmail = inngest.createFunction(
{
id: 'send-welcome-email',
name: 'Send Welcome Email',
// 🎯 Configurações de retry
retries: {
attempts: 3,
backoff: 'exponential', // 1s, 2s, 4s, 8s...
},
// ⏱️ Timeout para a function
timeout: '30s',
},
// 🎧 Event trigger - escuta event específico
{ event: 'user/signup.completed' },
// 🔧 Handler da function
async ({ event, step, logger }) => {
const { user, metadata } = event.data;
// 📝 Log estruturado
logger.info('Processing welcome email', {
userId: user.id,
email: user.email
});
// 📧 Step 1: Enviar email de boas-vindas
const emailResult = await step.run('send-welcome-email', async () => {
return await sendEmail({
to: user.email,
subject: 'Welcome to Our SaaS! 🎉',
template: 'welcome',
data: {
userName: user.firstName,
loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
supportEmail: 'support@mysaas.com'
}
});
});
// 📊 Step 2: Registrar analytics (opcional)
await step.run('track-welcome-email-sent', async () => {
// Analytics tracking
return await fetch('/api/analytics/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'welcome_email_sent',
userId: user.id,
properties: {
emailId: emailResult.id,
timestamp: new Date().toISOString()
}
})
});
});
// ✅ Retorno com dados do resultado
return {
success: true,
emailId: emailResult.id,
userId: user.id,
sentAt: new Date().toISOString()
};
}
);🎯 Passo 5: Disparando Events
📡 Como Enviar Events
Events podem ser enviados de qualquer lugar: API routes, Server Actions, webhooks, ou até outras functions. Sempre use type-safe events.
1. From API Route (User Signup)
import { inngest } from '@/inngest/client';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { email, firstName, lastName } = await request.json();
// 1️⃣ Criar usuário no banco
const user = await prisma.user.create({
data: { email, firstName, lastName }
});
// 2️⃣ Disparar event para background jobs
await inngest.send({
name: 'user/signup.completed',
data: {
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
},
metadata: {
source: 'web_signup',
timestamp: new Date().toISOString(),
userAgent: request.headers.get('user-agent')
}
}
});
return NextResponse.json({
success: true,
userId: user.id
});
} catch (error) {
console.error('Signup error:', error);
return NextResponse.json(
{ error: 'Signup failed' },
{ status: 500 }
);
}
}2. From Server Action
'use server';
import { inngest } from '@/inngest/client';
import { auth } from '@clerk/nextjs/server';
export async function updateUserProfile(formData: FormData) {
const { userId } = await auth();
if (!userId) {
throw new Error('Unauthorized');
}
// Update user profile...
const updatedUser = await updateUser(userId, formData);
// 🎯 Trigger background job para sync com CRM
await inngest.send({
name: 'user/profile.updated',
data: {
userId,
changes: updatedUser,
updatedAt: new Date().toISOString()
}
});
return { success: true };
}3. From Webhook Handler
import { inngest } from '@/inngest/client';
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response(`Webhook error: ${err}`, { status: 400 });
}
// 🎯 Enviar para Inngest processar de forma robusta
await inngest.send({
name: 'stripe/webhook.received',
data: {
type: event.type,
stripeEventId: event.id,
payload: event.data,
receivedAt: new Date().toISOString()
}
});
return new Response('OK');
}🔥 Passo 6: Functions Avançadas
⏰ Scheduled Functions
Functions que executam em horários específicos, como relatórios mensais ou limpeza de dados. Cron jobs serverless com observabilidade.
import { inngest } from '../client';
export const generateMonthlyReport = inngest.createFunction(
{
id: 'generate-monthly-report',
name: 'Generate Monthly Report',
// ⏰ Executa no primeiro dia de cada mês às 9h
cron: '0 9 1 * *',
// 🚨 Function crítica - retry mais agressivo
retries: {
attempts: 5,
backoff: 'exponential'
},
// ⏱️ Relatórios podem demorar
timeout: '10m',
},
// 🎧 Trigger por schedule (cron)
{ cron: '0 9 1 * *' },
async ({ step, logger }) => {
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1);
logger.info('Starting monthly report generation', {
month: lastMonth.toISOString()
});
// 📊 Step 1: Coletar dados de usuários
const userStats = await step.run('collect-user-stats', async () => {
return await prisma.user.groupBy({
by: ['createdAt'],
where: {
createdAt: {
gte: lastMonth,
lt: now
}
},
_count: true
});
});
// 💰 Step 2: Coletar dados financeiros
const revenueStats = await step.run('collect-revenue-stats', async () => {
return await prisma.subscription.findMany({
where: {
createdAt: {
gte: lastMonth,
lt: now
}
},
include: {
plan: true
}
});
});
// 📈 Step 3: Gerar relatório
const report = await step.run('generate-report', async () => {
return {
period: {
start: lastMonth.toISOString(),
end: now.toISOString()
},
metrics: {
newUsers: userStats.length,
revenue: revenueStats.reduce((sum, sub) => sum + sub.plan.price, 0),
churn: calculateChurn(revenueStats)
}
};
});
// 📧 Step 4: Enviar para stakeholders
await step.run('send-report-email', async () => {
return await sendEmail({
to: ['ceo@company.com', 'cfo@company.com'],
subject: `Monthly Report - ${lastMonth.toLocaleDateString()}`,
template: 'monthly-report',
data: { report }
});
});
return { success: true, reportId: report.id };
}
);🔄 Multi-Step Workflows
Workflows complexos com múltiplos steps, delays e condicionais. Cada step é automaticamente retried e observável.
import { inngest } from '../client';
export const userOnboardingFlow = inngest.createFunction(
{
id: 'user-onboarding-flow',
name: 'User Onboarding Flow',
},
{ event: 'user/signup.completed' },
async ({ event, step, logger }) => {
const { user } = event.data;
// 📧 Step 1: Welcome email imediato
await step.run('send-welcome-email', async () => {
return await sendEmail({
to: user.email,
template: 'welcome',
data: { user }
});
});
// ⏰ Step 2: Aguardar 1 dia
await step.sleep('wait-one-day', '1d');
// 🔍 Step 3: Verificar se completou onboarding
const onboardingStatus = await step.run('check-onboarding', async () => {
return await prisma.user.findUnique({
where: { id: user.id },
select: { onboardingCompleted: true }
});
});
// 🎯 Step 4: Conditional - só envia se não completou
if (!onboardingStatus?.onboardingCompleted) {
await step.run('send-onboarding-reminder', async () => {
return await sendEmail({
to: user.email,
template: 'onboarding-reminder',
data: { user }
});
});
// ⏰ Step 5: Aguardar mais 3 dias
await step.sleep('wait-three-days', '3d');
// 🔍 Step 6: Check novamente
const finalCheck = await step.run('final-onboarding-check', async () => {
return await prisma.user.findUnique({
where: { id: user.id },
select: { onboardingCompleted: true }
});
});
// 📞 Step 7: Se ainda não completou, trigger sales call
if (!finalCheck?.onboardingCompleted) {
await step.run('schedule-sales-call', async () => {
return await inngest.send({
name: 'sales/call.requested',
data: {
userId: user.id,
reason: 'onboarding_incomplete',
priority: 'medium'
}
});
});
}
}
return { success: true, userId: user.id };
}
);📊 Passo 7: Monitoring e Debugging
🎯 Inngest Dashboard
Dashboard visual completo para monitorar functions, debug failures, ver metrics em tempo real e gerenciar retries.
📈 Metrics Disponíveis
- • Executions: Total, success, failed
- • Duration: Avg, p95, p99
- • Throughput: Events/min, functions/min
- • Retries: Count, success rate
- • Queued: Pending jobs
- • Errors: Rate, types, frequency
🐛 Debugging Features
- • Visual Timeline: Cada step visualizado
- • Logs Estruturados: Por function/step
- • Replay Functions: Re-run com mesmo event
- • Pause/Resume: Pausar execução
- • Manual Retries: Force retry specific function
- • Event History: Todos events enviados
Logging Best Practices
export const exampleFunction = inngest.createFunction(
{ id: 'example-function' },
{ event: 'example/event' },
async ({ event, step, logger }) => {
// 📝 Log estruturado no início
logger.info('Function started', {
eventId: event.id,
userId: event.data.userId,
timestamp: new Date().toISOString()
});
// 🔧 Log em cada step importante
const result = await step.run('process-data', async () => {
logger.debug('Processing data', {
dataSize: event.data.items.length
});
try {
const processed = await processData(event.data);
// ✅ Log de sucesso
logger.info('Data processed successfully', {
itemsProcessed: processed.length,
duration: Date.now() - startTime
});
return processed;
} catch (error) {
// ❌ Log de erro com contexto
logger.error('Data processing failed', {
error: error.message,
stack: error.stack,
input: event.data
});
throw error; // Re-throw para trigger retry
}
});
// 🎯 Log final com métricas
logger.info('Function completed', {
success: true,
resultCount: result.length,
totalDuration: Date.now() - functionStartTime
});
return result;
}
);🔗 Passo 8: Integração com tRPC
🎯 tRPC + Inngest
Combine tRPC procedures com Inngest functions para APIs que respondem rápido mas processam de forma robusta em background.
1. tRPC Procedure que Dispara Job
import { createTRPCRouter, protectedProcedure } from '@/trpc/init';
import { inngest } from '@/inngest/client';
import { z } from 'zod';
export const reportsRouter = createTRPCRouter({
// 📊 Generate report - resposta imediata, processing em background
generateReport: protectedProcedure
.input(z.object({
type: z.enum(['monthly', 'quarterly', 'annual']),
filters: z.object({
startDate: z.string(),
endDate: z.string(),
includeChurn: z.boolean().default(true)
}).optional()
}))
.mutation(async ({ ctx, input }) => {
const reportId = generateId();
// 1️⃣ Criar record de report (status: pending)
const report = await prisma.report.create({
data: {
id: reportId,
type: input.type,
status: 'pending',
userId: ctx.auth.userId,
filters: input.filters,
createdAt: new Date()
}
});
// 2️⃣ Disparar background job
await inngest.send({
name: 'report/generation.requested',
data: {
reportId,
userId: ctx.auth.userId,
type: input.type,
filters: input.filters
}
});
// 3️⃣ Resposta imediata ao cliente
return {
reportId,
status: 'pending',
message: 'Report generation started. You will be notified when ready.'
};
}),
// 📋 Get report status
getReportStatus: protectedProcedure
.input(z.object({ reportId: z.string() }))
.query(async ({ ctx, input }) => {
const report = await prisma.report.findFirst({
where: {
id: input.reportId,
userId: ctx.auth.userId
}
});
if (!report) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Report not found'
});
}
return {
id: report.id,
status: report.status,
progress: report.progress,
downloadUrl: report.downloadUrl,
createdAt: report.createdAt,
completedAt: report.completedAt
};
})
});2. Inngest Function que Processa
import { inngest } from '../client';
export const generateReport = inngest.createFunction(
{
id: 'generate-report',
name: 'Generate Report',
retries: { attempts: 3 },
timeout: '15m' // Reports podem demorar
},
{ event: 'report/generation.requested' },
async ({ event, step, logger }) => {
const { reportId, userId, type, filters } = event.data;
// 📊 Step 1: Update status to processing
await step.run('update-status-processing', async () => {
return await prisma.report.update({
where: { id: reportId },
data: {
status: 'processing',
startedAt: new Date()
}
});
});
// 📈 Step 2: Collect data (pode ser lento)
const reportData = await step.run('collect-report-data', async () => {
logger.info('Collecting data for report', { reportId, type });
// Simulate complex data processing
const data = await collectReportData(type, filters);
// Update progress
await prisma.report.update({
where: { id: reportId },
data: { progress: 50 }
});
return data;
});
// 📄 Step 3: Generate PDF/Excel (pode ser lento)
const fileUrl = await step.run('generate-report-file', async () => {
logger.info('Generating report file', { reportId });
const file = await generateReportFile(reportData, type);
// Update progress
await prisma.report.update({
where: { id: reportId },
data: { progress: 80 }
});
return file.url;
});
// ✅ Step 4: Update status to completed
await step.run('update-status-completed', async () => {
return await prisma.report.update({
where: { id: reportId },
data: {
status: 'completed',
downloadUrl: fileUrl,
completedAt: new Date(),
progress: 100
}
});
});
// 📧 Step 5: Notify user via email
await step.run('notify-user', async () => {
const user = await prisma.user.findUnique({
where: { id: userId }
});
return await sendEmail({
to: user.email,
subject: 'Your report is ready! 📊',
template: 'report-ready',
data: {
reportType: type,
downloadUrl: fileUrl,
userName: user.firstName
}
});
});
return {
success: true,
reportId,
downloadUrl: fileUrl
};
}
);3. Frontend com Polling
'use client';
import { useTRPC } from '@/trpc/client';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useState } from 'react';
export function ReportGenerator() {
const [reportId, setReportId] = useState<string | null>(null);
const trpc = useTRPC();
// 🚀 Mutation para gerar report
const generateMutation = useMutation({
...trpc.reports.generateReport.mutationOptions(),
onSuccess: (data) => {
setReportId(data.reportId);
}
});
// 🔄 Polling para status do report
const { data: reportStatus } = useQuery({
...trpc.reports.getReportStatus.queryOptions({
reportId: reportId!
}),
enabled: !!reportId,
refetchInterval: (data) => {
// Stop polling quando completo
return data?.status === 'completed' ? false : 2000;
}
});
const handleGenerate = () => {
generateMutation.mutate({
type: 'monthly',
filters: {
startDate: '2025-01-01',
endDate: '2025-01-31'
}
});
};
return (
<div className="space-y-4">
<button
onClick={handleGenerate}
disabled={generateMutation.isPending}
className="btn btn-primary"
>
{generateMutation.isPending ? 'Starting...' : 'Generate Report'}
</button>
{reportStatus && (
<div className="bg-bg-2 p-4 rounded">
<p>Status: {reportStatus.status}</p>
{reportStatus.progress && (
<div className="w-full bg-bg-3 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${reportStatus.progress}%` }}
/>
</div>
)}
{reportStatus.status === 'completed' && (
<a
href={reportStatus.downloadUrl}
className="btn btn-success mt-2"
download
>
Download Report
</a>
)}
</div>
)}
</div>
);
}✅ Inngest Mastery Completo
🎯 O que Você Dominou:
- ✅ Event-driven architecture com Inngest
- ✅ Background jobs robustos e confiáveis
- ✅ Scheduled functions (cron serverless)
- ✅ Multi-step workflows complexos
- ✅ Retry automático e error handling
- ✅ Monitoring e debugging visual
- ✅ Integração seamless com tRPC
- ✅ Type-safety completo end-to-end
🚀 Próximos Passos:
- • Implementar em projeto real
- • Configurar alertas e monitoring
- • Adicionar mais event types
- • Otimizar performance das functions
- • Integrar com mais services externos
- • Setup CI/CD para functions