ETags e cache condicional em APIs Laravel (PHP 8.3): reduzir banda e latência
Por John Pires · 20 de março de 2026

1) Motivacao e trade-offs
1.1 Por que ETags importam em APIs modernas
ETags permitem que uma API diga ao cliente "minha representação do recurso não mudou desde a última vez que você consultou" sem enviar o payload completo. Para APIs REST que retornam JSON grandes ou coleções, isso reduz uso de banda e latência percebida. Em mobile ou conexões móveis, a diferença entre 200 com corpo e 304 sem corpo é perceptível.
Além da redução de dados, ETags permitem combinar semântica de versionamento leve com HTTP condicional: o servidor decide se um recurso foi alterado e pode responder 304 Not Modified. Isso é simples de integrar com clientes HTTP que respeitam cabeçalhos padrão (If-None-Match / If-Modified-Since).
1.2 Trade-offs e custos
Calcular fingerprints custa CPU e memória. Gerar um hash baseado no corpo da resposta exige serializar e hashear — isso pode ser custoso em respostas muito grandes ou com alta taxa de requisições. Alternativa: usar metadados (timestamps, contadores, version columns) para gerar fingerprints mais baratos.
Outro trade-off é a validade do ETag em presença de compressão e proxies: o ETag deve representar a entidade efetiva. Se o servidor fizer gzip dinamicamente, ETag pode divergir entre versões comprimidas e não comprimidas (use ETag fraca ou calcule sobre a representação canônica).
Prática: prefira ETags gerados a partir de metadados (updated_at, count, checksum armazenado) quando possível; reserve hashing do corpo para casos simples.
2) Fundamentos tecnicos
2.1 Cabeçalhos HTTP relevantes
Para cache condicional você precisa entender:
- ETag: identificador da representação. Pode ser forte ("v1") ou fraco (W/"v1").
- If-None-Match: enviado pelo cliente, contém ETags conhecidas. Se algum corresponder, servidor retorna 304.
- Last-Modified e If-Modified-Since: alternativa baseada em timestamp; menos precisa em mudanças rápidas.
- Cache-Control e Vary: instruem caches intermediários; Vary é crítico quando resposta depende de cabeçalhos (Authorization, Accept-Encoding).
2.2 Estratégias para gerar fingerprints
Principais estratégias:
- Hash do corpo (SHA1/SHA256): simples e preciso, CPU-intenso.
- Combinação de metadados: updated_at + count + relacionamento_checksum. Mais rápida, requer disciplina no modelo.
- Fingerprint armazenado (precomputed): manter checksum atualizado ao salvar/alterar recursos (ex: coluna etag_token ou Redis). Excelente performance em leitura.
Escolha depende de perfil: alta taxa de leitura -> precompute; respostas pequenas -> hash do corpo aceitável.
3) Exemplo 1: caso simples
3.1 Contexto e objetivo
Exemplo direto: API que retorna um recurso singular (UserResource) e gera ETag com hash do JSON. Ideal para endpoints que não são acionados em alta frequência ou que retornam payloads razoáveis.
3.2 Implementação minimal em middleware
Antes do código: o middleware captura a resposta, calcula SHA1 do conteúdo e adiciona cabeçalho ETag. Se o cliente enviar If-None-Match igual, retorna 304 sem corpo.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ETagResponseMiddleware
{
public function handle(Request $request, Closure $next)
{
/** @var Response $response */
$response = $next($request);
// Só para respostas 200 com corpo JSON
if ($response->getStatusCode() !== 200 || stripos($response->headers->get('Content-Type'), 'application/json') === false) {
return $response;
}
$body = $response->getContent();
$etag = '"' . sha1($body) . '"';
$ifNoneMatch = $request->headers->get('if-none-match');
if ($ifNoneMatch && trim($ifNoneMatch) === $etag) {
$notModified = new Response('', 304);
// Mantenha os cabeçalhos de cache
$notModified->headers->set('ETag', $etag);
$notModified->headers->set('Cache-Control', 'private, max-age=0, must-revalidate');
return $notModified;
}
$response->headers->set('ETag', $etag);
$response->headers->set('Cache-Control', 'private, max-age=0, must-revalidate');
return $response;
}
}
Depois do código: registre o middleware em app/Http/Kernel.php (route middleware) e aplique nas rotas API que precisam de ETag.
<?php
// app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'etag' => \App\Http\Middleware\ETagResponseMiddleware::class,
];
Aplicação na rota:
<?php
// routes/api.php
Route::get('users/{user}', function (App\Models\User $user) {
return new App\Http\Resources\UserResource($user);
})->middleware('etag');
Resultado: cliente com If-None-Match igual ao ETag recebe 304. Sem cabeçalho, recebe 200 + ETag.
4) Exemplo 2: caso avancado do mundo real
4.1 Contexto: coleções paginadas e relações
Problema real: endpoint /posts retorna uma coleção paginada com autor, comentários count e filtros. Recalcular hash do corpo a cada requisição é custoso; entretanto, mudanças parciais (comentário novo) devem invalidar ETag.
Solução: gerar fingerprint da query baseada em:
- max(updated_at) da coleção (maior timestamp entre posts retornados)
- count total (paginacao)
- versao de cache das relações (ex: comments_count token)
4.2 Implementação: Fingerprint service + Redis cache
Antes do código: encapsulamos a lógica em um serviço que recebe um Builder e retorna um token. O token pode ser armazenado em Redis com TTL curto para evitar recalcular constantemente.
<?php
namespace App\Services;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Redis;
class QueryFingerprint
{
public function fingerprint(Builder $builder, string $cacheKey): string
{
// Use SQL + bindings como parte do fingerprint para consultas complexas
$sql = $builder->toSql();
$bindings = json_encode($builder->getBindings());
// Pegue metadados úteis: maior updated_at e count
$clone = clone $builder;
$maxUpdated = $clone->getQuery()->newQuery()->fromSub($builder->toBase(), 't')->max('updated_at') ?: '';
$count = $builder->toBase()->getCountForPagination();
$raw = $sql . '|' . $bindings . '|' . $maxUpdated . '|' . $count;
$key = 'fingerprint:' . sha1($raw);
$cached = Redis::get($key);
if ($cached) {
return $cached;
}
$token = '"' . sha1($raw . ':' . now()->timestamp) . '"';
// TTL de 30s para reduzir pressão de recalculo; ajustar conforme perfil
Redis::setex($key, 30, $token);
return $token;
}
}
Depois do código: no controller, você injeta o serviço, gera fingerprint e responde condicionalmente.
<?php
// App/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Services\QueryFingerprint;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class PostController
{
public function index(Request $request, QueryFingerprint $fingerprintService): JsonResponse
{
$query = Post::withCount('comments')->where('status', 'published');
// Apply filters / sorts / pagination here
$page = (int) $request->query('page', 1);
$perPage = 20;
$paginated = $query->orderBy('published_at', 'desc')->paginate($perPage, ['*'], 'page', $page);
$token = $fingerprintService->fingerprint($query, 'posts_list_' . $page);
$ifNoneMatch = $request->headers->get('if-none-match');
if ($ifNoneMatch && trim($ifNoneMatch) === $token) {
return response()->json([], 304)->header('ETag', $token);
}
$response = response()->json($paginated);
$response->header('ETag', $token);
$response->header('Cache-Control', 'private, max-age=0, must-revalidate');
return $response;
}
}
Explicação: o fingerprint combina SQL + bindings + metadados. Armazenamos token em Redis com TTL curto. Quando posts, comentários ou edições atualizam updated_at, o token muda no próximo recalculo e clientes recebem nova versão.
4.3 Integração com eventos e precompute
Para reduzir recalculações, atualize um token pré-computado no momento de escrita (listeners de modelo):
<?php
// App/Listeners/PostChanged.php
namespace App\Listeners;
use Illuminate\Support\Facades\Redis;
use App\Models\Post;
class PostChanged
{
public function handle(Post $post)
{
// Exemplo simples: invalidate a chave global
Redis::del('fingerprint:' . sha1('posts_list'));
// Ou atualize um token específico
Redis::set('posts_token', '"' . sha1($post->updated_at . ':' . time()) . '"');
}
}
Resultado: operações de escrita mantêm tokens coerentes sem recalcular queries em leitura.
5) Debugging e troubleshooting
5.1 Testes e inspeção com ferramentas
Teste sempre com clientes reais:
- curl -I para ver cabeçalhos apenas
- curl -H "If-None-Match: \"abc\"" para simular condicionais
- Laravel HTTP tests para asserts automatizados
Exemplo de teste Laravel (PHP) para assert 304:
<?php
public function test_posts_returns_304_when_etag_matches()
{
$user = User::factory()->create();
// Assume que o service já gerou token '"abc"' em Redis
Redis::set('fingerprint:posts', '"abc"');
$response = $this->withHeaders(['If-None-Match' => '"abc"'])->getJson('/api/posts');
$response->assertStatus(304);
$this->assertEquals('"abc"', $response->headers->get('ETag'));
}
Depois do código: esse teste valida comportamento de 304 e a presença do cabeçalho ETag na resposta vazia.
5.2 Erros comuns e armadilhas
Liste das armadilhas que aparece com frequência:
- ETag calculado sobre corpo comprimido: se você calcula depois de gzip, proxies podem entregar versões diferentes; prefira calcular sobre representação canônica.
- Vary não configurado quando resposta depende de Authorization ou Accept-Language — caches intermediários podem servir conteúdo inválido.
- Usar hashing do corpo em endpoints com alta taxa de tráfego e corpo grande: CPU-bound.
- Comparar ETags sem normalizar aspas/weak prefix (W/): falhas de matching causam 200 desnecessários.
- Headers removidos por proxies/CDNs: alguns CDNs eliminam If-None-Match ou manipulam ETag. Teste no ambiente de produção.
Dica prática: para endpoints autenticados, prefira Cache-Control: private e evite caches intermediários que não lidam corretamente com Authorization.
Se o ETag for sensível a compressão, marque-o como fraco: prefixe com W/. Indique no Cache-Control se a resposta é privada.
6) Conclusao pratica
ETags são uma ferramenta pragmática para reduzir banda e latência em APIs Laravel quando corretamente aplicados. Use hashing direto para casos simples, mas prefira fingerprints baseados em metadados ou tokens pré-computados para coleções e endpoints de alta demanda.
Implemente por camadas: middleware simples para recursos singulares, serviços para coleções e listeners para manter tokens atualizados em operações de escrita. Combine com cabeçalhos Cache-Control e Vary apropriados.
Teste sempre com clientes reais e no ambiente de produção, porque proxies e CDNs costumam alterar comportamento de headers. Monitore CPU e latência ao introduzir hashing do corpo; se for problemático, migre para precompute ou metadados.
Checklist final:
- Escolha a estratégia de fingerprint adequada (hash | metadata | precompute).
- Implemente comparação correta de If-None-Match com normalização.
- Adicione Cache-Control e Vary conforme necessidade.
- Teste com curl e Laravel tests para garantir 304/200 corretos.
Implementando essas práticas você reduz tráfego desnecessário, melhora a experiência do cliente e mantém controle fino sobre a invalidação de cache sem introduzir complexidade desnecessária na camada de leitura.
Assuntos: PHP, Laravel, etag, http-cache, api
Leia também
Filament: importar CSV com jobs na fila e mostrar progresso em Livewire
Tutorial prático para iniciantes: configure fila no Laravel, crie upload no Filament, dispare um Job que processa CSV e atualize um componente Livewire com…
Laravel 10: consumir APIs externas com Http Client, retries, fallback e cache
Guia prático para iniciantes em Laravel 10 sobre como consumir APIs externas usando Http Client: timeout, retries, cache e fallback, com exemplos errados e…
Testes de Jobs e Queues no Laravel: Queue::fake(), dispatch e assertions
Guia prático e passo a passo para testar jobs e queues no Laravel usando Queue::fake(), dispatch e assertions. Aprenda a isolar dispatches, validar payloads e…
Autenticação de API com Laravel Sanctum para SPA e mobile — passo a passo
Guia prático para autenticar SPA e apps mobile com Laravel Sanctum: compare o jeito errado (token no localStorage) com o certo (cookies para SPA, tokens com…
APIs type-safe no Laravel: DTOs tipados, Value Objects e validação centralizada
Implementação prática para APIs type-safe em Laravel: DTOs readonly, Value Objects (Email, Money), validação centralizada com FormRequest e resolução…
Circuit breaker para APIs externas com Laravel Http Client e Redis
Implementação prática de circuit breaker em Laravel usando Redis e Http Client. Mostra o jeito errado (flag em memória, retries inline) e a solução robusta…