ISR on-demand escalável no Next.js App Router com webhooks, middleware e filas
Por John Pires · 29 de março de 2026

Cenario pratico do dia a dia
Você tem um site Next.js (App Router) que publica artigos, produtos ou páginas que são caros para renderizar em cada request. O time de conteúdo edita diretamente no CMS (Contentful, Sanity, Strapi, etc.) e espera que as mudanças apareçam no site imediatamente — sem invalidar todo o cache do CDN e sem rebuild do site. O objetivo é revalidar apenas as rotas afetadas, de forma confiável, escalável e segura.
Este artigo mostra um fluxo pragmático para implementar ISR on-demand escalável: webhooks que enfileiram eventos, um worker que consome a fila e chama uma rota interna de revalidação, e middleware para proteger e filtrar solicitações. Tudo usando recursos do App Router (route handlers, revalidatePath/revalidateTag) e uma fila simples baseada em Redis.
Problema que resolvemos
Revalidações diretas do CMS (webhooks chamando seu /api/revalidate) funcionam até um ponto. Quando o volume cresce, você precisa de: deduplicação, retry/backoff, controle de taxa e separação de responsabilidades (webhook -> fila -> worker -> revalidate). Se uma revalidação falhar no pico, você não quer perder o evento.
Também precisamos proteger o endpoint de revalidação para que apenas um worker autenticado (ou um serviço internal) possa disparar revalidatePath/revalidateTag. Isso evita abuso e evita que a CDN seja forçada a limpar cache sem controle.
Restrições do App Router
No App Router, as funções utilitárias revalidatePath e revalidateTag podem ser chamadas em route handlers e server components. Porém, se você executar um worker fora do ambiente do Next.js (container separado ou lambda), esse worker não pode chamar revalidatePath diretamente — ele precisa chamar uma rota exposta pelo Next.js que então chama revalidatePath internamente.
Por isso a arquitetura proposta coloca a revalidação efetiva dentro do processo Next.js, enquanto a fila e o worker ficam em infra independente (ou mesmo um processo Node separado), consumindo de Redis e chamando a rota de revalidação com um segredo.
Arquitetura da solucao
A solução tem 4 componentes principais: o webhook receptor, a fila (Redis), o worker consumidor e o endpoint de revalidação dentro do Next.js. Opcionalmente, middleware para validação e rate-limiting precoce.
Componentes e responsabilidades
- Webhook (app/api/webhook/route.ts): recebe chamadas do CMS, valida assinatura e enfileira um job no Redis (lista ou stream).
- Fila (Redis): armazena eventos com TTL; usada para dedupe e visibilidade mínima.
- Worker (processo Node separado): consome a fila, aplica deduplicação (SETNX), faz retries exponenciais, e chama a rota de revalidação do Next.js.
- Revalidate endpoint (app/api/revalidate/route.ts): endpoint interno que verifica um segredo e chama revalidatePath ou revalidateTag.
Fluxo básico
- CMS envia webhook ao
/api/webhook. - Webhook valida assinatura e empurra payload para a fila Redis.
- Worker faz BRPOP ou XREAD da fila; aplica dedupe e chama
/api/revalidatecom um token secreto. - Endpoint de revalidação chama
revalidatePathourevalidateTag, e responde. CDN atualiza apenas a rota específica.
Esse design garante que sua aplicação Next.js execute a chamada de revalidação (o único lugar onde o Next runtime pode invalidar o cache), enquanto o worker gerencia confiabilidade e escala.
Codigo completo com explicacoes
1) Webhook: valida e enfileira
Este é o handler do App Router que recebe webhooks do CMS. Ele valida HMAC (se o CMS fornece) e salva o job no Redis usando LPUSH. O worker consumirá via BRPOP. A lógica aqui é deliberadamente simples — delegue retry/tempo de vida ao worker.
/* app/api/webhook/route.ts
Recebe webhooks do CMS, valida assinatura e push para a lista Redis 'isr:queue'.
*/
import { NextRequest, NextResponse } from 'next/server';
import Redis from 'ioredis';
import crypto from 'crypto';
const redis = new Redis(process.env.REDIS_URL!);
const SECRET = process.env.WEBHOOK_SECRET || 'change-me';
export async function POST(req: NextRequest) {
const body = await req.json();
const signature = req.headers.get('x-signature') || '';
// Exemplo HMAC simples
const expected = crypto.createHmac('sha256', SECRET).update(JSON.stringify(body)).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return new NextResponse('invalid signature', { status: 401 });
}
// Normaliza payload mínimo
const job = {
id: body.id || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
path: body.path || `/posts/${body.slug}`,
tag: body.tag || null,
ts: Date.now(),
};
await redis.lpush('isr:queue', JSON.stringify(job));
return NextResponse.json({ ok: true, enqueued: true });
}
Depois desse handler, o job fica na fila. Se a validação falhar, devolvemos 401 para que o CMS saiba que houve problema.
2) Middleware: proteção e rate filter
Usar middleware para bloquear requisições óbvias e evitar gastar CPU do app com chamadas inválidas. Aqui mostramos um middleware que permite apenas chamadas para /api/revalidate quando o header secreto está presente. Em ambientes Edge, evite operações bloqueantes no middleware.
/* middleware.ts
Bloqueia chamadas ao /api/revalidate sem o header x-internal-secret.
*/
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const url = req.nextUrl.clone();
if (url.pathname.startsWith('/api/revalidate')) {
const secret = req.headers.get('x-internal-secret');
if (!secret || secret !== process.env.INTERNAL_SECRET) {
return new NextResponse('unauthorized', { status: 401 });
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/revalidate'],
};
Colocar essa verificação no middleware evita carregar a rota se o segredo estiver faltando e reduz superfície de ataque.
3) Revalidate endpoint: chama revalidatePath / revalidateTag
Endpoint que efetivamente executa revalidações usando utilitários do Next.js. Ele espera um POST com { path } ou { tag } e só aceita chamadas internas (middleware já checou o segredo). Use revalidateTag quando você usa tags em fetches; use revalidatePath para rotas específicas.
/* app/api/revalidate/route.ts
Rota que chama revalidatePath ou revalidateTag. Deve receber x-internal-secret.
*/
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(req: NextRequest) {
const body = await req.json();
if (body.path) {
try {
revalidatePath(body.path);
return NextResponse.json({ ok: true, revalidated: body.path });
} catch (err) {
return NextResponse.json({ ok: false, error: String(err) }, { status: 500 });
}
}
if (body.tag) {
try {
revalidateTag(body.tag);
return NextResponse.json({ ok: true, revalidatedTag: body.tag });
} catch (err) {
return NextResponse.json({ ok: false, error: String(err) }, { status: 500 });
}
}
return NextResponse.json({ ok: false, error: 'missing path or tag' }, { status: 400 });
}
RevalidatePath/Tag são assíncronos no sentido de sinalizar ao Next para invalidar. O handler retorna rapidamente; ainda assim trate erros e logue para observabilidade.
4) Worker: consumo e retries
Exemplo de worker Node.js (executado fora do Next.js). Ele faz BRPOP, tenta deduplicar com SETNX e faz POST para /api/revalidate. Em falha, aplica exponential backoff e re-enfileira ou move para uma lista de falhas.
/* worker/worker.ts
Script Node que consome a lista Redis 'isr:queue' e chama o endpoint interno do Next.js.
*/
import Redis from 'ioredis';
import fetch from 'node-fetch';
const redis = new Redis(process.env.REDIS_URL!);
const INTERNAL_URL = process.env.INTERNAL_URL!; // e.g. https://app.example.com/api/revalidate
const INTERNAL_SECRET = process.env.INTERNAL_SECRET!;
async function loop() {
while (true) {
const item = await redis.brpop('isr:queue', 0); // [key, value]
const job = JSON.parse(item[1]);
const dedupeKey = `isr:dedupe:${job.id || job.path}`;
// tenta marcar como processando por 30s
const locked = await redis.set(dedupeKey, '1', 'NX', 'EX', 30);
if (!locked) continue; // já processado recentemente
try {
const body = job.tag ? { tag: job.tag } : { path: job.path };
const res = await fetch(INTERNAL_URL, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-internal-secret': INTERNAL_SECRET,
},
body: JSON.stringify(body),
timeout: 10000,
});
if (!res.ok) {
// simples retry: coloca no fim da fila com contador
job.retries = (job.retries || 0) + 1;
if (job.retries > 5) {
await redis.lpush('isr:dead', JSON.stringify({ ...job, failedAt: Date.now() }));
} else {
// backoff exponencial em milissegundos
const backoff = Math.min(60_000, 500 * 2 ** job.retries);
await redis.set(`isr:retry:${job.id}`, JSON.stringify(job), 'EX', Math.ceil(backoff / 1000));
// em vez de sleep, re-enfileira para processar depois
setTimeout(() => redis.lpush('isr:queue', JSON.stringify(job)), backoff);
}
}
} catch (err) {
// tratamento similar ao erro HTTP
job.retries = (job.retries || 0) + 1;
if (job.retries > 5) {
await redis.lpush('isr:dead', JSON.stringify({ ...job, error: String(err) }));
} else {
const backoff = Math.min(60_000, 500 * 2 ** job.retries);
setTimeout(() => redis.lpush('isr:queue', JSON.stringify(job)), backoff);
}
} finally {
// garante que o dedupe expirará naturalmente; caso precise liberar antes:
// await redis.del(dedupeKey);
}
}
}
loop().catch((err) => {
console.error('worker loop crashed', err);
process.exit(1);
});
Esse worker é simples, mas suficiente para começar. Em produção você pode usar BullMQ para visibilidade, métricas e UI de jobs, ou um serviço gerenciado.
Tratamento de erros e edge cases
Retries, idempotencia e dedup
Principais pontos práticos:
- Use SETNX no Redis para marcar jobs processados recentemente e evitar duplicates quando o CMS dispara vários webhooks para a mesma atualização.
- Implemente contador de retries no job; depois de N tentativas mova para uma fila
isr:deade alerte time de SRE/monitoramento. - Evite revalidar blocos inteiros — prefira tags ou paths granulares. RevalidateTag quando múltiplas rotas compartilham o mesmo conteúdo.
Regra prática: evitar revalidações em massa. Prefira revalidar o menor conjunto possível de rotas/tags e confie na CDN para servir conteúdo stale enquanto a revalidação acontece.
Segurança e abuso
Erros comuns e como evitá-los:
- Não exponha o endpoint /api/revalidate sem autenticação: middleware + header secreto é mínimo. Para maior segurança, combine com uma lista de IPs do worker ou um mTLS se possível.
- Não execute operações custosas no webhook: apenas valide e enfileire. O worker deve executar as chamadas que podem falhar e necessitar de retry/backoff.
- Proteja o Redis com ACLs e credenciais e limite TTLs de chaves temporárias.
Outra armadilha: confiar em revalidatePath para rotas dinâmicas sem garantir que o path existe. Sempre normalize o path antes de chamar revalidatePath; tente armazenar canonical paths no CMS.
Performance e otimizacao
Estratégias de cache e granularidade
Para minimizar cache churn e maximizar cache hit rate:
- Use tags para agrupar conteúdo que muda junto (ex.: tag user-123 para todas as páginas que dependem do perfil daquele usuário).
- Revalide apenas o path afetado; se o conteúdo é fragmentado, use revalidateTag para disparar revalidações de vários componentes.
- Sete revalidate no fetch quando fizer sentido:
fetch(url, { next: { revalidate: 60 } })para permitir cache controlado sem precisar de on-demand sempre.
Combinar ISR automático (revalidate interval) com on-demand para atualizações críticas costuma oferecer o melhor trade-off entre latência e consistência.
Observability, métricas e alertas
Instrumente o worker e a rota de revalidação com métricas básicas: número de jobs enfileirados, tempo de processamento, taxas de falha e tamanho da fila. Alertas no aumento de erros/filas permitem agir antes do usuário reportar inconsistências.
Log structureado (JSON) facilita correlação entre o evento original do CMS e o job processado no worker. Inclua job.id e traceId quando possível.
Erros comuns e dicas praticas
Algumas pegadinhas que vimos em projetos reais:
- Esquecer de proteger
/api/revalidate— leva a abuso e requests massivos que invalidam cache. - Colocar logic de retry no webhook em vez do worker — resulta em timeouts e duplicação de carga no app.
- Usar revalidatePath com path mal formatado (ex.: falta de barra no começo) — a chamada falha silenciosamente.
Dica: adicione um healthcheck na fila (/api/queue/health) que o SRE pode usar para saber se o worker está consumindo e quantos itens há na fila.
Conclusao
Implementar ISR on-demand escalável no Next.js App Router é mais sobre arquitetura do que sobre usar uma API específica. Separe responsabilidades: o webhook deve validar e enfileirar, o worker deve garantir entrega e retry, e o Next.js deve executar a revalidação do cache. Combine revalidatePath e revalidateTag com lógica de dedupe e backoff e você terá um sistema robusto que escala.
Resumo prático:
- Proteja o endpoint de revalidação com middleware e segredos.
- Enfileire eventos — não tente revalidar direto no webhook.
- Deduplicate via Redis SETNX e mova jobs falhos para uma fila dead-letter após N tentativas.
- Use tags para revalidar grupos de conteúdo e paths para rotas específicas.
Os exemplos fornecidos são uma base funcional: adapte o worker para usar BullMQ se precisar de dashboard e retries avançados, ou um serviço gerenciado se não quiser manter processos. O importante é evitar revalidações em massa e manter controle sobre quem pode invalidar o cache.
Assuntos: Next.js, app-router, isr, revalidate, webhooks, NoSQL
Leia também
ISR on-demand com Server Actions: revalidação via webhook e middleware
Como migrar de endpoints de revalidate inseguros para uma arquitetura segura com Server Actions, webhooks assinados e middleware em Next.js App Router…
Preview CMS com Next.js App Router: Server Actions, Middleware e ISR na prática
Implementar preview CMS no Next.js App Router exige cookies seguros, middleware para marcar requisições e uso combinado de ISR e Server Actions. Neste artigo…
SSR multi-região no Next.js App Router: sincronizar ISR e invalidar caches
Guia prático para sincronizar ISR em implantações multi-região com Next.js App Router: padrões, Server Actions, endpoint de revalidate, broadcast entre…
A/B testing no Next.js (App Router): middleware, cookies e ISR passo a passo
Guia prático para implementar A/B testing no Next.js App Router: use middleware para atribuir variantes, cookies para persistência e rewriting para habilitar…
Formulários sem JavaScript com Server Actions no Next.js (App Router)
Aprenda a criar formulários totalmente funcionais sem JavaScript usando Server Actions no Next.js (App Router). Exemplo prático com código, validação…
Agrupamento de requisições no App Router com Server Actions e Edge Cache
Padrões práticos para agrupar requisições no App Router usando Server Actions e Edge Cache: batch endpoints, ISR (revalidate), retries, deduplicação e…