Destino Dev

Blog

SSR multi-região no Next.js App Router: sincronizar ISR e invalidar caches

Por John Pires · 14 de março de 2026

SSR multi-região no Next.js App Router: sincronizar ISR e invalidar caches

1) Problema real que resolve

Contexto

Arquiteturas multi-região procuram reduzir latência e aumentar disponibilidade replicando a aplicação em várias regiões. No Next.js com App Router, usamos SSR/ISR para gerar páginas e armazená-las em cache nas bordas. Quando um conteúdo é atualizado, precisamos garantir que todas as regiões invalidem suas cópias para evitar servir conteúdo stale a usuários próximos a outras regiões.

Por que é difícil

Problemas práticos que aparecem em produção:

  • ISR é ótimo para desempenho, mas a invalidação padrão é limitada à instância que executou o revalidate. Em setups multi-região, cada região mantém cache local.

  • Se a atualização é feita numa região, outras regiões continuam a servir o conteúdo antigo até a próxima revalidação automática (TTL).

  • Invalidar globalmente exige coordenação entre regiões ou purga do CDN/global cache, o que nem sempre é trivial ou barato.

Ao final, o objetivo é: garantir que, após uma alteração (ex: editar um post), todas as regiões revalidem ou purguem o cache relacionado, evitando inconsistência e janelas longas de conteúdo obsoleto.

2) Abordagem ingênua vs abordagem otimizada

Abordagem ingênua

A solução mais simples é confiar apenas no TTL do ISR (ex: revalidate: 60). Funciona em casos tolerantes a inconsistências temporárias, mas:

  • Você terá uma janela de inconsistência até o TTL expirar em cada região.

  • Se atualizações são frequentes, TTL baixo aumenta custos de re-render e latência.

  • Não resolve casos onde é necessário ver o conteúdo atualizado imediatamente (ex: publicação de notícias, e-commerce).

Outra variação ingênua é chamar o endpoint de revalidate apenas na região onde a escrita ocorreu. Isso não alcança as outras regiões.

Abordagem otimizada e patterns práticos

Boas práticas que implementamos para mitigar problemas:

  1. Use revalidatePath / revalidateTag do Next.js no App Router para invalidar rotas ou tags específicas localmente.

  2. Ao atualizar um recurso, dispare uma rotina que notifique todas as regiões (lista controlada de endpoints regionais) para chamar o endpoint de revalidate local em cada região.

  3. Proteja os endpoints de revalidação com um token secreto para evitar abusos públicos.

  4. Opte por tags de cache quando várias URLs compartilham o mesmo dado (por exemplo, o mesmo post em listagem e página detalhada), usando revalidateTag para granularidade e eficiência.

  5. Se a infra permitir, use um bus de mensagens (Redis Pub/Sub, SNS/SQS, Pub/Sub) para notificar regiões sem expor endpoints públicos que precisam de polling manual.

Na prática, a combinação mais simples e confiável é: Server Action que atualiza o DB + um broadcast (fetch para cada endpoint regional) que aciona app/api/revalidate/route.ts em cada região usando revalidatePath / revalidateTag. Essa abordagem é fácil de implementar e auditar.

3) Implementação com código real

Descrição geral

Vamos montar um fluxo completo com:

  • Server Action para criar/editar conteúdo e disparar invalidações.

  • Route API /api/revalidate que executa revalidatePath/revalidateTag localmente e exige um token.

  • Componente Server que usa fetch com next: { revalidate, tags } para ISR.

  • Middleware simples que adiciona/expõe a região e protege rotas.

1) Route API para revalidar (app/api/revalidate/route.ts)

O arquivo abaixo é um route handler do App Router. Ele aceita POST com um body indicando paths e tags a revalidar. Está protegido por um header x-revalidate-token. Usa revalidatePath e revalidateTag.

import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath, revalidateTag } from 'next/cache'

const TOKEN = process.env.REVALIDATE_TOKEN // secret compartilhado entre regiões

export async function POST(req: NextRequest) {
  const token = req.headers.get('x-revalidate-token')
  if (!token || token !== TOKEN) return NextResponse.json({ ok: false }, { status: 401 })

  const payload = await req.json()
  const { paths = [], tags = [] }: { paths?: string[]; tags?: string[] } = payload

  try {
    for (const path of paths) {
      // invalida a rota localmente
      revalidatePath(path)
    }
    for (const tag of tags) {
      // invalida todas as rotas que usam essa tag
      revalidateTag(tag)
    }
    return NextResponse.json({ ok: true })
  } catch (err) {
    return NextResponse.json({ ok: false, error: String(err) }, { status: 500 })
  }
}

O que esse bloco faz: valida o token, executa revalidatePath/revalidateTag na instância regional que recebeu a requisição. O endpoint não precisa saber de outras regiões — quem o chama é responsável por alcançar todas as regiões.

2) Server Action que atualiza conteúdo e notifica regiões

Exemplo de Server Action dentro de um arquivo de rota de edição. Ele atualiza o banco (ex: Prisma) e depois itera sobre uma lista de endpoints regionais para notificar revalidação.

"use server"
import { revalidatePath } from 'next/cache' // opcional usar localmente

const REGION_ENDPOINTS = process.env.REGION_ENDPOINTS?.split(',') || []
const TOKEN = process.env.REVALIDATE_TOKEN

export async function updatePostAction(postId: string, data: { title: string; body: string }) {
  // 1) Atualiza no DB (exemplo com Prisma)
  // prisma import e client supostos estar disponíveis
  await prisma.post.update({ where: { id: postId }, data })

  // 2) Invalida localmente (opcional) para ser rápido na mesma região
  try {
    revalidatePath(`/posts/${postId}`)
    revalidateTag(`post:${postId}`)
  } catch (e) {
    // se revalidate falhar localmente, continuar com broadcast
  }

  // 3) Broadcast simples: notifica todos os endpoints regionais
  const payload = { paths: [`/posts/${postId}`], tags: [`post:${postId}`] }
  await Promise.all(REGION_ENDPOINTS.map(ep =>
    fetch(`${ep}/api/revalidate`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', 'x-revalidate-token': TOKEN },
      body: JSON.stringify(payload),
      // não seguir redirects, timeouts controlados pelo infra
    })
  ))
}

O que acontece: após a escrita no banco, fazemos uma invalidação local imediata e disparos para cada endpoint regional. Essa abordagem é simples e determinística: cada região chama seu próprio handler /api/revalidate que executa os revalidate* internamente.

3) Componente Server consumindo dados com ISR e tags

Com o App Router, você controla ISR no fetch via opções next. Use tags para agrupar caches que representam o mesmo dado.

export default async function PostPage({ params }: { params: { id: string } }) {
  const id = params.id
  const res = await fetch(`${process.env.API_URL}/posts/${id}`, {
    next: { revalidate: 60, tags: [`post:${id}`] }
  })
  const post = await res.json()

  return (
    {post.title}
  )
}

Explicação: esse fetch gera ISR com TTL de 60 segundos e marca a resposta com a tag post:{id}. Quando revalidateTag('post:{id}') for chamado, todas as rotas que referenciam essa tag serão revalidadas localmente.

4) Middleware para adicionar metadata de região e segurança

O middleware ajuda a identificar a região da instância e pode bloquear chamadas indesejadas. Exemplo simples que passa a región via header interno.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(req: NextRequest) {
  // REGIONAL_NAME configurado por pipeline (ex: "us-east-1", "eu-west-1")
  const region = process.env.REGIONAL_NAME || 'unknown'
  const res = NextResponse.next()
  res.headers.set('x-instance-region', region)
  return res
}

export const config = { matcher: '/' }

Após esse middleware, você pode logar a região e ajudar nos diagnósticos quando uma inconsitência entre regiões for reportada.

4) Testes e validação

Testes manuais

Procedimento mínimo para validar em staging:

  1. Configure duas implantações regionais de staging (ex: eu-west-1 e us-east-1) apontando para a mesma base de dados.

  2. Garanta que ambas tenham a mesma variável REGION_ENDPOINTS com os endpoints uma da outra.

  3. Edite um post na região A (usar Server Action).

  4. Verifique logs para chamadas de broadcast ao /api/revalidate nas duas regiões.

  5. Abra a URL do post em clientes próximos de cada região (ou use curl com header Host/geo mocking) e confirme que o conteúdo atualizado aparece imediatamente em ambas as regiões.

Com esses passos você valida o fluxo completo: escrita -> broadcast -> revalidate local.

Testes automatizados e carga

Automatize com scripts que:

  • Chamam a Server Action para atualizar conteúdo.

  • Interceptam chamadas HTTP para os endpoints regionais e confirmam recebimento.

  • Depois de uma pequena janela (ex: 1s), requestam a rota publicamente e verificam se o conteúdo mudou.

Para teste de carga, foque em:

  • Quantos broadcasts por segundo sua solução gera: limitação do número de endpoints, retries e timeouts é importante.

  • Quão rápido as revalidations locais ocorrem — instrumente logs ao chamar revalidatePath/revalidateTag.

Ferramentas: k6 para carga, Playwright para checks end-to-end e um mock HTTP server para capturar os hits nos endpoints regionais.

5) Considerações de produção

Erros comuns, armadilhas e dicas práticas

Lista de problemas que vejo com frequência:

  • Esquecer de proteger o endpoint de revalidate: sem token, atacantes podem forçar re-renders e gerar custos elevados.

  • Timeouts no broadcast: se você espera respostas síncronas de muitos endpoints, a atualização ficará lenta. Fire-and-forget assíncrono com retries é melhor.

  • Loop de revalidação: cuidado com revalidations que acionam webhooks que por sua vez revaliam novamente. Use flags ou verifique mudanças reais antes de rebroadcast.

  • Dependência de um único ponto: evitar um controlador central que seja single point of failure. Use lista de endpoints configurada e fallback para pub/sub.

  • TTL confuso vs revalidateTag: combinar TTL baixo e revalidateTag mal aplicado pode gerar re-renders desnecessários. Escolha tags para alterações acionáveis e TTL para performance.

Tip: prefira invalidar por tag quando a mesma entidade aparece em múltiplas rotas (listing + detail). Tags reduzem chaves a serem gerenciadas e evitam purga em massa.

Observability e métricas

Métricas essenciais para manter sob controle:

  • Taxa de chamadas a /api/revalidate por região.

  • Latência média das chamadas de broadcast e das revalidations locais.

  • Taxa de falhas nos revalidates (401/5xx).

  • Cache hit ratio por região e por tag (se possível).

Logs estruturados para cada revalidate com campos: region, postId/tag, duration, result_code. Use tracing (ex: OpenTelemetry) para ligar a atualização no servidor à invalidação nas regiões.

Escalabilidade e custos

Broadcast para muitas regiões tem custo e overhead operacional. Estratégias para reduzir custo:

  • Batch invalidations quando várias atualizações ocorrem numa janela curta.

  • Throttle/aggregate updates por entidade (ex: debounce 1s para edições rápidas).

  • Usar um serviço de mensagens global (SNS, Pub/Sub) para fanout eficiente em escala.

Conclusão prática

Resumindo o que importa na prática:

  • Não confie apenas no TTL do ISR em ambientes multi-região quando consistência rápida é necessária.

  • Combine revalidatePath/revalidateTag com um mecanismo de broadcast confiável para alcançar todas as regiões.

  • Proteja endpoints, monitore métricas e evite designs síncronos que aumentam latência da escrita.

Implementação recomendada: Server Action atualiza DB → invalidação local imediata → broadcast assíncrono para endpoints regionais (ou pub/sub global) → cada região executa revalidatePath/revalidateTag. Essa abordagem é simples, audível e testável sem depender de features específicas do provedor de hospedagem.

Adote tags para granularidade, proteja suas rotas de revalidação e instrumente tudo. Assim você reduz janelas de conteúdo stale, mantém latência baixa para usuários finais e tem controle operacional sobre invalidações.

Referências rápidas:

  • Next.js App Router: uso de fetch com next.revalidate e tags.

  • next/cache: revalidatePath e revalidateTag.

  • Padrões de pub/sub para sincronização multi-região.

Implementando o fluxo acima você terá um sistema previsível: atualizações geram invalidações determinísticas nas regiões, reduzindo a necessidade de TTL agressivamente baixo e controlando custos de re-render.

Assuntos: Next.js, app-router, isr, ssr, middleware, server-actions

Leia também