
Cache no Next.js 16: use cache, cacheLife e cacheTag
Cache no Next.js sempre teve fama de caixa-preta. No 13 e no 14, o fetch cacheava tudo sem pedir licença. No 15, parou de cachear por padrão. E aí você tinha revalidate, dynamic = 'force-static', unstable_cache e next: { tags }, cada um com suas regras. O Next.js 16 junta tudo isso num modelo só, explícito: nada é cacheado até você escrever 'use cache'. Neste post eu mostro como as três peças (use cache, cacheLife e cacheTag) se encaixam, e onde a gente costuma tropeçar.
Primeiro passo: ligar o Cache Components
O modelo novo vem atrás de uma flag. Ela substitui o antigo experimental.ppr:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Com a flag ligada, cada pedaço da página cai em uma de três categorias:
- Estático: código síncrono e imports, renderizados no build.
- Cacheado: dados assíncronos marcados com
'use cache'. - Dinâmico: tudo que depende da requisição (
cookies(),headers(),searchParams), envolto em<Suspense>.
O Next monta uma casca estática, preenche com o que está em cache e deixa a parte dinâmica chegar por streaming. Isso é o Partial Prerendering, agora sem flag experimental.

use cache: o que entra no cache
A diretiva funciona em três níveis: arquivo, componente e função. Eu prefiro usar em funções de dados, porque fica claro o que está sendo guardado:
// lib/posts.ts
import { cacheLife, cacheTag } from 'next/cache'
type PostSummary = {
slug: string
title: string
date: string
}
export async function getPosts(locale: string): Promise<PostSummary[]> {
'use cache'
cacheLife('hours')
cacheTag('posts', `posts-${locale}`)
const response = await fetch(`https://api.example.com/posts?locale=${locale}`)
if (!response.ok) {
throw new Error(`API respondeu ${response.status}`)
}
return response.json()
}
Repare que não tem chave de cache em lugar nenhum. O Next monta a chave sozinho, a partir do build, da função e dos argumentos serializáveis. getPosts('pt') e getPosts('en') são duas entradas diferentes, sem você fazer nada. Quem já escreveu um array de keyParts no unstable_cache sabe o alívio que é isso.
cacheLife: quanto tempo o dado vale
Sem cacheLife, vale o perfil default. Os perfis prontos são 'seconds', 'minutes', 'hours', 'days', 'weeks' e 'max'. Quando nenhum serve, você passa os três tempos na mão:
import { cacheLife } from 'next/cache'
export async function getExchangeRate(currency: string): Promise<number> {
'use cache'
cacheLife({
stale: 60, // o navegador usa a cópia local por até 1 minuto
revalidate: 300, // depois de 5 minutos, o servidor rebusca em segundo plano
expire: 3600, // depois de 1 hora sem acesso, a entrada some
})
const response = await fetch(`https://api.example.com/rates/${currency}`)
const data: { rate: number } = await response.json()
return data.rate
}
Pensa nos três números como a validade de um pão na padaria: stale é quanto tempo você come sem nem olhar, revalidate é quando o padeiro já começa a assar outro enquanto você ainda come o velho, e expire é quando vai para o lixo de vez.
cacheTag: dar nome para poder apagar depois
Tempo resolve o caso comum. Mas quando alguém publica um post, você não quer esperar uma hora. É aí que entram as tags: você marca a entrada e invalida pelo nome.
O Next 16 separa a invalidação em duas funções, e escolher a errada é o tropeço mais comum:
'use server'
import { revalidateTag, updateTag } from 'next/cache'
import { savePost } from '@/lib/db'
import { requireAdmin } from '@/lib/auth'
export async function publishPost(locale: string, slug: string, mdx: string): Promise<void> {
await requireAdmin()
await savePost(locale, slug, mdx)
// quem publicou precisa ver o post novo nesta mesma resposta
updateTag(`posts-${locale}`)
}
export async function importPosts(): Promise<void> {
await requireAdmin()
// ... importação em lote
// ninguém está esperando: serve o velho e atualiza em segundo plano
revalidateTag('posts', 'max')
}
updateTag: expira na hora, e a mesma requisição já lê o dado novo. Só funciona em Server Actions. É o "salvei e quero ver".revalidateTag: marca como velho e atualiza em segundo plano (stale-while-revalidate). Funciona também em Route Handlers, então serve para webhooks. No 16, o segundo argumento com o perfil passou a ser esperado; a forma com um argumento só está obsoleta.
O cache que você não vê
Uma lição que aprendi neste próprio site: o cache do Next não é o único da cadeia. Aqui o blog lê os posts de uma API separada, com tag posts e um webhook que chama revalidateTag a cada publicação. Tudo certo no papel. Mesmo assim, um post publicado ficou dando 404.
O motivo: a API mandava s-maxage=60, stale-while-revalidate=300. O webhook invalidava a tag, o site rebuscava na hora... e recebia do CDN da API a cópia de antes da escrita. Essa cópia velha ficava guardada por mais uma hora.
// ❌ Errado: a API de origem cacheada no CDN, por baixo do cache do Next
reply.header('Cache-Control', 's-maxage=60, stale-while-revalidate=300')
// ✅ Correto: o Next é a única camada de cache; a origem responde sempre fresca
reply.header('Cache-Control', 'no-store')
cacheTag invalida o cache do Next, não o do mundo. Se tem CDN, proxy ou cache de banco no meio, a invalidação precisa passar por todos eles. Ou então só uma camada cacheia.
Erros comuns
Ler cookies dentro do cache
// ❌ Errado: cookies() dentro de 'use cache' dá erro
async function CartSummary() {
'use cache'
const cartId = (await cookies()).get('cart')?.value
const cart = await getCart(cartId)
return <p>{cart.items.length} itens</p>
}
// ✅ Correto: lê fora e passa como argumento (vira parte da chave)
async function CartPage() {
const cartId = (await cookies()).get('cart')?.value ?? 'anon'
return <CartSummary cartId={cartId} />
}
async function CartSummary({ cartId }: { cartId: string }) {
'use cache'
const cart = await getCart(cartId)
return <p>{cart.items.length} itens</p>
}
Usar revalidateTag quando o usuário espera ver a mudança
// ❌ Errado: o autor salva e a tela ainda mostra a versão antiga
revalidateTag(`post-${slug}`, 'max')
// ✅ Correto: numa Server Action, leitura do que acabou de escrever
updateTag(`post-${slug}`)
Valores não determinísticos dentro do cache
// ❌ Errado: o horário fica congelado no momento em que a entrada foi criada
async function Footer() {
'use cache'
return <p>Gerado em {new Date().toLocaleTimeString('pt-BR')}</p>
}
✅ Se o valor precisa mudar a cada requisição, ele fica fora do 'use cache', num componente dinâmico dentro de <Suspense>.
Esquecer a tag
❌ Cachear com cacheLife('days') e nenhuma cacheTag. ✅ Se o dado muda por ação de alguém, ele precisa de uma tag. Senão, a única forma de atualizar é esperar ou fazer um novo deploy.
Quando usar e quando evitar
| Situação | O que eu faria |
|---|---|
| Lista de posts, catálogo, páginas de doc | 'use cache' + cacheLife('hours') + tag |
| Dado que muda por ação do admin | tag + updateTag na Server Action |
| Webhook de CMS ou de outra API | revalidateTag(tag, 'max') no Route Handler |
| Carrinho, sessão, dado por usuário | dinâmico em <Suspense>, sem cache |
| Cotação, placar, estoque em tempo real | cacheLife('seconds') ou dinâmico |
Origem que já tem CDN com s-maxage | escolha uma camada só para cachear |
Próximos passos
- Ligue
cacheComponents: truenum branch e rode o build: os erros apontam exatamente onde há acesso dinâmico sem<Suspense>. - Troque cada
unstable_cachepor uma função com'use cache', levandotagsparacacheTagerevalidateparacacheLife. - Para cada
cacheTag, pergunte: quem invalida isso, e o usuário precisa ver na hora? A resposta escolhe entreupdateTagerevalidateTag. - Confira os headers da sua origem. Se a API tem
s-maxage, você tem dois caches, e só invalida um deles.
Cache bom é cache que você consegue explicar. Se ninguém do time sabe responder "por quanto tempo isso fica velho e quem apaga", vale parar e desenhar isso antes de ligar a flag. E se você já migrou algum projeto para o modelo novo, me conta como foi, quero saber onde você tropeçou.
André Luiz Vieira
Engenheiro Frontend e Mobile
Moro em Curitiba e há cinco anos construo produtos em React e React Native para bancos, cooperativas e e-commerce. Aqui eu escrevo sobre o que aprendo no caminho.
Bora conversar?
Dúvidas, ideias ou uma visão diferente? Os comentários usam o GitHub Discussions; entre com sua conta do GitHub para comentar.