Destino Dev

Blog

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

  1. CMS envia webhook ao /api/webhook.
  2. Webhook valida assinatura e empurra payload para a fila Redis.
  3. Worker faz BRPOP ou XREAD da fila; aplica dedupe e chama /api/revalidate com um token secreto.
  4. Endpoint de revalidação chama revalidatePath ou revalidateTag, 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:dead e 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:

  1. 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.
  2. 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.
  3. 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