MÓDULO 14 · CONCEITO 10 DE 12

CDN e Cache Distribuído — latência global e o problema difícil da invalidação

Por que servir um arquivo da costa oeste dos EUA para um usuário em Singapura leva 200ms quando a luz só precisa de 80ms — e como CDN reduz isso para 20ms. Arquitetura multi-camada de cache: browser → CDN edge → origin shield → cache de aplicação → banco. Cache invalidation, descrito por Phil Karlton como uma das duas coisas difíceis em computação. Estratégias: TTL, purge por evento, versioning. CDN para APIs (não só para imagens estáticas). Cache stampede e o problema do thundering herd. Patterns: cache-aside, read-through, write-through, write-behind. Consistência em caches distribuídos.

Tempo de leitura ~30 min Pré-requisito 09 · Database Internals · módulo 06 (Cache) Próximo 11 · Consistência Eventual Avançada →

Em 1995, Tim Berners-Lee, o inventor da web, deu uma palestra no MIT com uma previsão sombria: a internet ia colapsar sob o próprio peso. O número de usuários estava dobrando a cada poucos meses, mas a infraestrutura de servidores não acompanhava. Um aluno na plateia, Tom Leighton, professor de matemática aplicada, levou a preocupação a sério e começou a pesquisar algoritmos de roteamento e caching distribuído com seu aluno de doutorado, Daniel Lewin. Em 1998 fundaram a Akamai — palavra havaiana para "inteligente" — com uma ideia que parecia absurda na época: copiar o conteúdo da internet para milhares de servidores espalhados pelo mundo e fazer cada usuário acessar a cópia mais próxima. Em setembro de 1999, Apple anunciou um keynote streaming online; o site da Apple foi para o ar com a Akamai e suportou um pico de 100x o tráfego normal sem cair. A indústria entendeu: CDNs não eram um nice-to-have, eram infraestrutura crítica. Hoje, Akamai serve ~30% do tráfego web global, Cloudflare outros ~20%, e literalmente toda empresa de tecnologia de algum porte depende de pelo menos uma CDN.

O insight central que torna CDNs tão eficazes é matemático e brutal: a velocidade da luz é fixa. Um pacote de dados levando São Paulo → Tóquio leva no mínimo ~75ms ida-e-volta apenas pelo tempo físico (latência inerente à distância, mesmo em fibra óptica perfeita). Adicione roteamento, congestionamento e overhead de protocolo: na prática são 200-300ms. Para uma página que faz 20 requisições, isso é o suficiente para uma experiência terrível, independentemente de como o backend está otimizado. CDN é a única forma conhecida de driblar a velocidade da luz: em vez de mandar o usuário até o conteúdo, leve o conteúdo até o usuário. Não há atalho algorítmico que substitua proximidade física.

Requisitos e estimation

Diferente de outros conceitos de system design (URL shortener, messaging), cache não é um sistema isolado — é uma camada que atravessa todo o stack. O "design" aqui é dimensionar capacidade por camada, definir SLOs de latência e hit ratio, e justificar o custo de infraestrutura de cache vs o custo de não ter cache.

Cenário hipotético usado para os números abaixo:

# Estimation por camada — sistema multi-cache:

# CDN EDGE (Cloudflare/CloudFront tier típico):
# - Throughput: precisa absorver 15k req/s pico globalmente
# - Distribuição PoPs: 200+ PoPs cobrem <30ms para 95% da população mundial
# - Cache storage por PoP: 1-5TB (SSD), tipicamente 10-50GB hot tier em RAM
# - Hit ratio target: 90% para estáticos, 60% para dinâmicos cacheáveis
#   → backend recebe: 15k × (0.6 × 0.1 + 0.3 × 0.4 + 0.1 × 1.0) = 15k × 0.28 = ~4.2k req/s
# - Custo CDN: ~$0.08/GB primeiros 10TB, decresce em volume
#   * 10B requests × 50KB médio = 500TB/mês
#   * Cache hit: servido do edge, custo direto da CDN ($40k/mês neste range)
#   * Cache miss: também custa origin egress (banda do AWS/GCP do backend)

# ORIGIN SHIELD (camada intermediária):
# - 5-10 regiões consolidando misses dos 200+ edge PoPs
# - Hit ratio shield: 80%+ dos misses do edge são absorvidos aqui
#   → backend recebe: 4.2k × 0.2 = ~840 req/s (de 15k originais, 94% absorvidos)
# - Custo: tier separado da CDN, geralmente 50-100% mais caro que edge cache

# REDIS CLUSTER (cache de aplicação distribuído):
# - Dimensionamento: hot working set × 1.5 (folga para crescimento e fragmentação)
#   * Hot working set típico: 10-20% do total de dados ativos
#   * Se DB tem 500GB, hot working set ~50-100GB
#   * Redis cluster: 100GB × 1.5 = 150GB RAM total
#   * Distribuído em 6 shards (25GB cada) com RF=2 = 12 nodes
# - Throughput: 100k+ ops/s por shard; cluster atinge 600k+ ops/s facilmente
# - Latência: P99 <5ms LAN; P50 <1ms
# - Custo: AWS ElastiCache r6g.xlarge (~$0.30/h por node) × 12 = ~$2600/mês

# IN-PROCESS CACHE (per app instance):
# - Dimensionamento: 5-10% do heap por instance, ou 100MB-1GB
#   * Caffeine/LRU com 10k-100k entries
# - Hit ratio: 40-70% (recebe apenas requests que passaram pelo Redis miss)
# - Latência: sub-microssegundo (sem rede)
# - Custo: zero direto (memória da instância que já existe)

# Hit ratio cumulativo do stack (target):
# CDN edge:       80% absorvido aqui
# Origin shield:  16% adicional (80% dos misses do edge)
# Redis:           3% adicional (80% dos misses do shield)
# In-process:      0.5% (já estava cacheado em Redis na maioria)
# DB:              0.5% restante (cold misses)
# → DB recebe 0.5% do tráfego total = ~75 req/s vs 15k sem cache (200× redução)

# Cost analysis comparativo:
# CENÁRIO A — Sem cache:
#   - DB precisa aguentar 15k req/s: cluster Postgres com 10+ replicas, $20k+/mês
#   - Origin egress: 500TB/mês × $0.09/GB = $45k/mês
#   - Latência terrível para usuários distantes (200-500ms TTFB)
# CENÁRIO B — Com cache full stack:
#   - CDN: ~$40k/mês (a maior parte do custo, mas absorve 96% do tráfego)
#   - Origin shield: ~$5k/mês
#   - Redis cluster: ~$3k/mês
#   - DB single primary + 1-2 replicas: $3k/mês (recebe 0.5% do tráfego)
#   - Origin egress: 500TB × 4% miss × $0.09/GB = $1.8k/mês
#   - Total: ~$53k/mês vs $65k+ sem cache
#   - Bonus: latência 5-10× melhor globalmente, proteção contra DDoS incluída

# Memory sizing — quando o cache não cabe na RAM:
# Working set = 80GB, Redis cluster = 50GB → 80% das chaves vivem em memória, 20% miss
# Hit ratio efetivo cai porque chaves quentes podem ser evictadas pela "long tail"
# Regra prática: dimensionar para 1.5× o working set ativo (não para o total de dados)
# Working set ≠ total de dados: maioria dos sistemas tem 80/20 (80% dos reads são em 20% das chaves)

# Quando cache custa MAIS que não ter cache:
# - Sistemas write-heavy onde cache é constantemente invalidado (hit ratio <30%)
# - Sistemas com dados altamente personalizados (cada usuário é unique)
# - Sistemas pequenos onde DB single-instance aguenta toda a carga
# - Sistemas onde a infraestrutura de cache requer time dedicado de ops

A arquitetura multi-camada de cache

# Pipeline de cache em uma request típica:

  ┌─────────────────┐
  │ Browser cache   │  ← primeira camada (em RAM do dispositivo)
  └────────┬────────┘
           │ se miss
           ▼
  ┌─────────────────┐
  │ Service Worker  │  ← opcional (offline-first apps)
  └────────┬────────┘
           │
           ▼
  ┌─────────────────┐
  │ CDN Edge        │  ← ~200 PoPs globalmente, 1-50ms do usuário
  │ (local PoP)     │
  └────────┬────────┘
           │ se miss
           ▼
  ┌─────────────────┐
  │ Origin Shield   │  ← regional cache, agrega misses (~5 regiões)
  │ (regional cache)│
  └────────┬────────┘
           │
           ▼
  ┌─────────────────┐
  │ Load Balancer   │  ← se chegou aqui, é cache miss em todas camadas
  └────────┬────────┘
           │
           ▼
  ┌─────────────────────────────────────────────────────────────────────┐
  │ Application Server                                                  │
  │   ├─ In-process cache (per-instance, em RAM do processo)            │
  │   ├─ Distributed cache (Redis/Memcached cluster)                    │
  │   └─ Database                                                       │
  └─────────────────────────────────────────────────────────────────────┘

# Latências aproximadas (cache hit em cada camada):
# Browser cache:    0ms (local lookup)
# Service Worker:   ~1ms
# CDN edge:         ~10-30ms (local PoP)
# Origin Shield:    ~30-60ms (regional)
# In-process cache: 1-5ms (sem rede)
# Redis cluster:    ~1-2ms (LAN)
# Database hit:     5-50ms (depende de query)
# Database miss + disk seek: 50-500ms

# Hit ratios típicos em sistemas bem desenhados:
# Browser:    30-50% (depende de etag/last-modified discipline)
# CDN edge:   80-95% para conteúdo estático
# CDN edge:   40-70% para conteúdo dinâmico cacheável
# App cache:  60-90% para hot data
# DB hit:     o restante

# IMPORTANTE: cada camada PROTEGE a próxima
# CDN com 95% hit ratio = backend recebe 5% do tráfego total
# Sem CDN: backend recebe 100% → 20x mais carga
# Por isso CDN não é só latência — é proteção contra DDoS, throttling de spike, custo de banda

Como CDN funciona internamente

# Componentes de uma CDN moderna (Cloudflare, Akamai, CloudFront, Fastly):

# 1. PoPs (Points of Presence):
#    - Datacenters espalhados pelo mundo (Cloudflare: 300+, Akamai: 4000+)
#    - Cada PoP tem servidores cache + roteamento + DDoS mitigation
#    - Usuário é roteado para o PoP mais próximo via:
#      * Anycast DNS (mesmo IP responde de múltiplos PoPs; BGP roteia ao mais próximo)
#      * GeoDNS (DNS responde com IP diferente baseado no IP do client)

# 2. Edge caching:
#    - Cada PoP tem armazenamento (SSD) com TBs de capacidade
#    - LRU (Least Recently Used) eviction quando enche
#    - Conteúdo popular fica em RAM hot tier; conteúdo morno em SSD
#    - Cold content: eviction → próxima request vai para origin

# 3. Origin Shield (camada intermediária):
#    - Entre os edge PoPs e o origin
#    - Agrega cache misses: se 50 edge PoPs têm cache miss simultâneo,
#      o origin shield consolida em 1 request ao origin
#    - Reduz carga no origin em 10-100x para conteúdo cacheável

# 4. Routing intelligence:
#    - BGP anycast: rotas BGP convergem para o PoP mais próximo
#    - Health checks: PoP com problema é removido da rotação
#    - Failover automático: se PoP local cai, próximo PoP assume

# Fluxo de uma request (cache miss):
# 1. user.com/image.jpg → DNS resolve para anycast IP
# 2. BGP roteia o pacote para o PoP mais próximo (ex: São Paulo)
# 3. PoP recebe a request, verifica cache local
# 4. Cache miss → consulta Origin Shield (ex: PoP de Miami)
# 5. Origin Shield cache miss → request ao origin (ex: Virginia)
# 6. Origin responde → Origin Shield armazena → PoP local armazena → user

# Fluxo de uma request (cache hit no edge):
# 1. user.com/image.jpg → BGP route para PoP local
# 2. PoP verifica cache → HIT
# 3. Responde direto ao usuário em 10-20ms
# (não toca origin nem Origin Shield)

# Cache key:
# Por padrão: URL completa (incluindo querystring)
# Custom: pode-se configurar para ignorar/incluir headers específicos
# Cuidado: cache key muito específico = baixo hit ratio
# Cache key muito amplo = entrega conteúdo errado (ex: ignorar user-agent quando
# o conteúdo difere por dispositivo)

Cache headers HTTP: o vocabulário da invalidação

# Os headers HTTP que definem o comportamento de cache:

# Cache-Control (RFC 7234):
# Diretivas para qualquer cache no caminho (browser, CDN, proxies)

# Para o RESPONSE (servidor → cliente):
Cache-Control: public, max-age=31536000, immutable
# - public: pode ser cacheado por shared caches (CDN), não só browser
# - max-age=31536000: TTL de 1 ano em segundos
# - immutable: conteúdo NUNCA muda; browser não precisa revalidar mesmo após max-age

Cache-Control: private, max-age=600
# - private: apenas browser do usuário pode cachear (não CDN)
# - max-age=600: 10 minutos
# - usado para conteúdo personalizado (logged-in user view)

Cache-Control: no-store
# - NUNCA cachear, em nenhum lugar
# - usado para dados sensíveis (banking, personal data)

Cache-Control: no-cache
# - pode cachear, mas deve revalidar com origin a cada request
# - useful: cache armazena, mas dispara conditional GET para verificar

Cache-Control: s-maxage=300, max-age=60
# - s-maxage: TTL para shared caches (CDN)
# - max-age: TTL para browser
# - Útil: CDN cacheia por 5min, browser por 1min (menos pressão de invalidação)

# Para o REQUEST (cliente → servidor):
Cache-Control: no-cache
# - cliente força revalidação (ex: shift+reload no browser)

# ETag — versão do recurso, opaco
ETag: "abc123"
# - Servidor gera hash do conteúdo (md5, sha1, ou contador)
# - Cliente envia em request seguinte com If-None-Match: "abc123"
# - Servidor: se conteúdo ainda é "abc123", retorna 304 Not Modified (sem body)
# - Cliente reutiliza o cached body, mas confirma frescor

# Last-Modified — alternativa baseada em timestamp
Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT
# - Cliente envia: If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT
# - Servidor: se não mudou desde então, 304 Not Modified
# - Granularidade de 1 segundo (ETag pode ser mais preciso)

# Vary — varia cache por header específico
Vary: Accept-Encoding, User-Agent
# - Cache deve manter versões separadas baseado nestes headers
# - Ex: gzipped vs uncompressed (Accept-Encoding)
# - CUIDADO: User-Agent tem milhares de variações → fragmenta cache extremamente
# - Vary: Cookie é quase sempre um erro (cada usuário tem cookie único)

# Age — quanto tempo este response está em cache
Age: 1234
# - CDN adiciona automaticamente; útil para debug

# Conditional requests reduzem banda mas NÃO latência:
# Ainda precisa do roundtrip ao origin para o 304
# Cache hit puro (sem revalidação) é estritamente melhor
# Por isso "Cache-Control: max-age=X, immutable" é o ideal para assets versionados

# Pattern moderno: versioning na URL + immutable
# /assets/app-a4f7c8.js  ← hash do conteúdo no nome
# Cache-Control: public, max-age=31536000, immutable
# Mudou o JS? Novo hash → nova URL → nova chave de cache → entrega imediata
# Sem invalidação necessária; cache antigo expira naturalmente

Cache invalidation: o problema difícil

Phil Karlton, engenheiro da Netscape, é famoso pela frase: "There are only two hard things in Computer Science: cache invalidation and naming things." É uma piada — e é verdade. Invalidação é difícil porque cada estratégia tem tradeoffs entre simplicidade, custo e consistência. Não existe solução universal.

# ESTRATÉGIA 1: TTL (Time-To-Live)
# A solução mais simples: cada entry tem um tempo de expiração
# Após o TTL, o cache invalida automaticamente

# Cache-Control: max-age=300  → 5 minutos

# Vantagens:
# - Trivial de implementar (toda CDN/cache suporta nativamente)
# - Sem coordenação entre origin e cache
# - Bounded staleness: pior caso, dado é stale por max_TTL

# Desvantagens:
# - Stale data garantido até o TTL expirar
# - Pode invalidar conteúdo que ainda é válido (waste de bandwith)
# - Para sistemas onde mudança é imprevisível: ou TTL curto (alta carga no origin)
#   ou TTL longo (mais stale)

# Quando usar: conteúdo que muda em intervalos previsíveis ou onde alguns segundos/
# minutos de stale são aceitáveis (a maior parte das aplicações web)

# ESTRATÉGIA 2: Purge / Invalidate explícito
# Quando o conteúdo muda no origin, mandar request ao cache para invalidar a chave

# CDN APIs:
# Cloudflare: POST /zones/{zone_id}/purge_cache  body: {"files": ["..."]}
# Akamai: Fast Purge API
# CloudFront: CreateInvalidation
# Fastly: API com surrogate keys

# Fluxo:
# 1. User atualiza produto no DB
# 2. App emite evento de cache invalidation
# 3. Worker assíncrono chama API da CDN para purgar /products/123
# 4. CDN remove da edge cache; próxima request vai ao origin

# Vantagens:
# - Cache sempre fresh (próximo request vê dado novo)
# - Pode-se usar TTL longo + purge: máximo hit ratio + invalidação imediata

# Desvantagens:
# - Lógica de invalidação espalhada (cada caminho de write precisa saber o que invalidar)
# - Propagação não é instantânea: 1-60 segundos típico em CDN globais
# - Falhas em purge → cache inconsistente (precisa retry robusto)
# - Caro em escala: purgar 1M chaves não é instantâneo, custa CPU na CDN

# Surrogate keys (Fastly, Varnish, etc):
# Cada response pode ter tags (surrogate keys), múltiplas
# Ex: response de /products/123 tem keys: ["product:123", "category:eletronics"]
# Purge por key: invalidação em massa (todos os caches relacionados a category:eletronics)
# Permite invalidação semântica em vez de URL-by-URL

# Quando usar: quando freshness é importante mas TTL longo é desejado por hit ratio
# (catálogos de e-commerce, conteúdo editorial)

# ESTRATÉGIA 3: Versioning na URL (cache busting)
# Em vez de invalidar a chave existente, criar nova chave para nova versão

# /app.v1.js → Cache-Control: max-age=1year, immutable
# Mudou: deploy gera /app.v2.js
# HTML atualizado: <script src="/app.v2.js">
# v1 ainda em cache de muitos usuários (válido), v2 é novo
# Não precisa "invalidar" v1; ela expira naturalmente

# Versão por hash do conteúdo:
# /app.a4f7c8.js  ← primeiros 6 chars do md5/sha do bundle
# Conteúdo mudou → hash muda → URL muda automaticamente
# Webpack, Rollup, Vite todos suportam content-hash em filename nativamente

# Vantagens:
# - Atomicidade de deploy (transição é instantânea, sem janela de inconsistência)
# - Cache hit ratio máximo (TTL = anos)
# - Sem necessidade de purge

# Desvantagens:
# - Só funciona para conteúdo que o cliente sabe a versão (assets carregados pelo HTML)
# - Não aplicável para APIs ou conteúdo dinâmico (cliente não sabe a versão a priori)
# - Custa storage no servidor (precisa manter versões antigas para clientes com HTML antigo)

# Quando usar: assets estáticos (JS, CSS, imagens com versão). Padrão da indústria.

# ESTRATÉGIA 4: Event-driven invalidation (Kafka + cache invalidation service)
# Para sistemas grandes: emitir eventos de mudança em barramento; consumidores
# invalidam caches relevantes

# 1. User atualiza profile → escrita no DB
# 2. CDC (Change Data Capture) ou outbox emite evento user.updated{id: 123}
# 3. Cache invalidation service consome:
#    - Purga CDN keys relacionadas
#    - DELETE em Redis: profile:123, profile:123:full, dashboard:123
#    - Notifica WebSocket workers para refresh local caches
# 4. Próxima leitura recarrega do DB

# Vantagens:
# - Decouple lógica de write do conhecimento de cache
# - Múltiplas camadas de cache invalidadas com mesmo evento
# - Audit trail (eventos persistidos)

# Desvantagens:
# - Infraestrutura adicional (Kafka, Debezium, workers)
# - Latência: evento → invalidação tem delay de segundos
# - Eventos perdidos = caches stale silenciosamente

Cache patterns: cache-aside, read-through, write-through, write-behind

# CACHE-ASIDE (lazy loading) — o pattern mais comum

def get_user(user_id):
    user = cache.get(f"user:{user_id}")
    if user is None:
        user = db.query("SELECT * FROM users WHERE id = ?", user_id)
        if user:
            cache.set(f"user:{user_id}", user, ttl=300)
    return user

def update_user(user_id, data):
    db.update("users", user_id, data)
    cache.delete(f"user:{user_id}")  # invalidação explícita

# Vantagens:
# + Cache só tem dados realmente lidos (não desperdiça com dados nunca acessados)
# + Resiliente a falhas de cache (se cache cai, aplicação ainda funciona)
# + Modelo mental simples
# Desvantagens:
# - Primeira request é sempre lenta (cache miss + DB)
# - Risco de cache stampede (ver abaixo)
# - Inconsistência possível: update DB + delete cache não são atômicos

# READ-THROUGH — cache é a interface

def get_user(user_id):
    return cache.read(f"user:{user_id}", loader=lambda: db.get_user(user_id))

# Cache library (ex: Caffeine no Java, ASP.NET MemoryCache, Spring Cache):
# - Em miss, chama o loader fornecido
# - Aplicação não vê DB diretamente; cache abstrai
# Vantagens:
# + Lógica de carga centralizada
# + Aplicação fica mais simples
# Desvantagens:
# - Acoplamento ao cache library
# - Loader complexo se queries são heterogêneas

# WRITE-THROUGH — toda escrita atualiza cache E DB sincronamente

def update_user(user_id, data):
    cache.set(f"user:{user_id}", data)
    db.update("users", user_id, data)
    # Se DB falha, cache pode estar inconsistente — usar transações distribuídas
    # ou ordem específica (DB primeiro, depois cache; em falha, invalidar)

# Vantagens:
# + Cache sempre tem dados frescos
# + Reads subsequentes são instantâneos
# Desvantagens:
# - Latência de write inclui ambos os sistemas
# - Cache pode encher com dados raramente lidos
# - Atomicidade entre cache e DB é problemática

# WRITE-BEHIND (write-back) — escrita assíncrona ao DB

def update_user(user_id, data):
    cache.set(f"user:{user_id}", data)
    write_queue.enqueue(("update", user_id, data))  # processado em background

# Vantagens:
# + Throughput de write altíssimo (write em RAM)
# + Pode coalescer múltiplas updates da mesma chave (batch writes)
# Desvantagens:
# - RISCO DE PERDA DE DADOS: se cache cai antes do write para DB, dados perdidos
# - Sistema fica vulnerável a corrupção em casos de crash
# - Complexo: precisa retry, durable queue, etc
# - Difícil de raciocinar sobre consistency

# Quando usar cada um:
# Cache-aside: 90% dos casos. Default razoável.
# Read-through: quando a lógica de cache é uniforme e centralizada faz sentido.
# Write-through: quando o produto precisa de cache sempre fresco e latência de write
#                aceitável. Bom para dados raramente atualizados mas muito lidos.
# Write-behind: muito específico — analytics, métricas, contadores onde perda mínima
#               é aceitável e throughput é crítico. Usar com queue durável.

Cache stampede: o problema do thundering herd

# Cenário: uma chave popular expira em cache.
# Antes que o primeiro request atualize o cache, 10000 outros requests chegam.
# Todos veem cache miss e vão ao DB simultaneamente.
# DB recebe 10000 queries idênticas → satura → cai.

# Em produção: foi assim que um single product page derrubou e-commerces inteiros.

# MITIGAÇÕES:

# MITIGAÇÃO 1: Probabilistic early expiration
# Em vez de TTL absoluto, atualizar o cache PROBABILISTICAMENTE perto do TTL
# (paper "Optimal Probabilistic Cache Stampede Prevention")

import random
import math

def get_with_early_refresh(key, ttl, beta=1.0):
    entry = cache.get(key)
    if entry is None:
        return refresh(key, ttl)

    now = time.time()
    delta = compute_function_time  # quanto tempo o loader leva
    expiry = entry["expires_at"]

    # Probabilidade de refresh aumenta à medida que se aproxima do expiry
    if now - beta * delta * math.log(random.random()) >= expiry:
        return refresh(key, ttl)
    return entry["value"]

# A função se comporta:
# - Bem antes do expiry: probability ~0 (quase nenhum request refresh)
# - Perto do expiry: probability cresce
# - No expiry: probability = 1 (todos os requests refresh)
# Resultado: apenas alguns requests refresh perto do expiry; a maioria continua usando cached

# MITIGAÇÃO 2: Single-flight (request coalescing)
# Apenas UM request por chave faz o trabalho; outros aguardam o resultado

import asyncio

class SingleFlight:
    def __init__(self):
        self.in_flight = {}  # key → future

    async def do(self, key, loader):
        if key in self.in_flight:
            return await self.in_flight[key]  # outro request já está carregando

        future = asyncio.create_task(loader())
        self.in_flight[key] = future
        try:
            return await future
        finally:
            del self.in_flight[key]

# Resultado: 10000 requests simultâneos → 1 query ao DB; outros 9999 esperam pelo mesmo
# Reduz carga no DB em ordens de magnitude

# Vantagens:
# + Drástico em redução de carga
# + Sem mudança de TTL ou lógica de cache
# Desvantagens:
# - Apenas funciona dentro de UM processo (in-memory dict)
# - Para múltiplos processos: precisa lock distribuído (Redis SETNX)

# MITIGAÇÃO 3: Lock distribuído com Redis
# Coordenar via Redis para que apenas um worker faça o refresh

async def get_with_lock(key, loader, ttl):
    value = await cache.get(key)
    if value is not None:
        return value

    # Cache miss — tentar adquirir lock para refresh
    lock_key = f"lock:refresh:{key}"
    locked = await redis.set(lock_key, "1", nx=True, ex=30)  # 30s lock TTL

    if locked:
        try:
            value = await loader()
            await cache.set(key, value, ttl=ttl)
            return value
        finally:
            await redis.delete(lock_key)
    else:
        # Outro processo está fazendo o refresh — aguardar
        for _ in range(50):  # max 5s
            await asyncio.sleep(0.1)
            value = await cache.get(key)
            if value is not None:
                return value
        # Fallback: ir direto ao DB (raro; geralmente quem locked já populou cache)
        return await loader()

# MITIGAÇÃO 4: Stale-while-revalidate (RFC 5861)
# Servir dado expirado por um tempo, enquanto refresh acontece em background

Cache-Control: max-age=300, stale-while-revalidate=60
# Por 300s: cache fresh
# Entre 300s e 360s: cache stale, mas serve mesmo assim; dispara refresh background
# Após 360s: bloqueia e refresh sync

# Implementado nativamente em Cloudflare, Fastly, Vercel, browsers modernos
# Trade-off claro: alguns segundos de stale em troca de zero cache stampede

# MITIGAÇÃO 5: TTL randomizado (jitter)
# Em vez de TTL fixo, adicionar ±20% aleatório
ttl = base_ttl * (0.8 + random.random() * 0.4)
# Evita que muitas chaves expirem ao mesmo tempo (ex: warmup global no startup)

CDN para APIs (não só conteúdo estático)

# Mito comum: "CDN é só para imagens e arquivos estáticos"
# Realidade: CDN modernas (Cloudflare, Fastly, CloudFront) cacheiam APIs efetivamente

# Quando faz sentido cachear APIs:
# - Dados públicos (não personalizados): catálogo de produtos, posts de blog
# - Mudança previsível (TTL razoável funciona): cotações de moeda (15min)
# - Hot data (alto volume de leitura): top stories, leaderboards
# - Tolerância a alguns segundos de staleness

# Quando NÃO faz sentido:
# - Dados personalizados (logged-in user view)
# - Dados que mudam por request (cart, real-time scores)
# - Dados sensíveis (banking, healthcare)
# - APIs com muitos query parameters únicos (cache fragmentation)

# Cache key para API:
# Default: URL + query string
# Cuidado: tracking parameters (utm_source, fbclid) fragmentam cache desnecessariamente
# Solução: configurar CDN para ignorar tracking params

# Vary headers para APIs:
Vary: Accept-Language    # ✓ se API retorna em vários idiomas
Vary: Accept-Encoding    # ✓ geralmente automático para gzip/br
Vary: Authorization      # ✗ NUNCA — cada token é único, cache morre
Vary: Cookie             # ✗ quase sempre erro
Vary: User-Agent         # ⚠ apenas se conteúdo realmente difere por agent

# Edge functions / Workers:
# CDNs modernas permitem rodar código JavaScript/WASM no edge
# Use cases:
# - Autenticação leve (validar JWT no edge sem ir ao origin)
# - Personalização (montar página com partes cached + partes user-specific)
# - A/B testing (atribuição no edge, sem afetar cache)
# - Geolocation (rotear para origin diferente baseado em país)
# - Image resizing on-the-fly

# Cloudflare Workers exemplo:
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const url = new URL(request.url)
  const cacheKey = new Request(url.toString(), request)
  const cache = caches.default

  let response = await cache.match(cacheKey)
  if (!response) {
    response = await fetch(request)
    if (response.ok) {
      const cachedResponse = response.clone()
      // Force cache for 5 minutes even if origin doesn't send Cache-Control
      cachedResponse.headers.set('Cache-Control', 'public, max-age=300')
      event.waitUntil(cache.put(cacheKey, cachedResponse))
    }
  }
  return response
}

# Personalization no edge:
# - Página = template cacheable (HTML body)
# - Pieces personalizadas via JS injetado no edge (nome do usuário, recommendations)
# - Reduz origin load enormemente para sites com partial personalization

Caches distribuídos: Redis e Memcached

# Memcached: o cache distribuído original (2003)
# - Único modelo: key → value (string ou bytes)
# - Sem persistência (puro in-memory)
# - LRU eviction nativo
# - Cluster: client-side sharding (consistent hashing no client)
# - Multi-threaded (escala com cores)
# - Sem TLS nativo, sem auth (precisa de wrapper)

# Use cases: cache simples de objetos, session storage, page fragments
# Sweet spot: cache puro, sem necessidade de estruturas complexas

# Redis: data structures + persistência + features ricas (2009)
# - Estruturas: string, list, hash, set, sorted set, stream, hyperloglog, geo, bitmap
# - Persistência opcional: RDB (snapshots) ou AOF (append-only log)
# - Replicação master-replica
# - Cluster com hash slots (16384 slots distribuídos entre masters)
# - Pub/Sub, Streams (alternativa lightweight a Kafka)
# - Lua scripting (operações atômicas multi-key)
# - TLS, auth, ACLs

# Use cases: cache + queue + leaderboard + rate limit + lock + pub/sub
# Sweet spot: quase tudo que precisa de in-memory data structure

# Redis Cluster: deep dive
# - 16384 hash slots distribuídos entre N masters
# - Hash slot de uma key = CRC16(key) mod 16384
# - Cliente "cluster-aware" cacheia o mapa slot → node
# - MOVED response: se request foi para shard errado, recebe nó correto
# - ASK response: durante resharding, key está movendo, retry no novo nó
# - Multi-key ops: só funcionam se todas as keys estão no mesmo slot
#   Solução: hash tags {user:123}:profile e {user:123}:settings ficam no mesmo slot

# Replicação:
# - Master → replicas (async por default)
# - Failover: Redis Sentinel monitora; se master cai, promove replica
# - Split-brain possível em partições — mitigação: min-slaves-to-write

# Persistência tradeoffs:
# RDB (snapshot): periódico, point-in-time, fast restart
#   - Risco: dados desde último snapshot podem ser perdidos
# AOF (append-only file): cada operação loggeada
#   - Fsync everysecond (default): perda máxima de 1s
#   - Fsync always: zero perda mas latência maior
# Recomendação para cache puro: nenhuma persistência (RAM only, falha cold start)
# Para uso como queue/source of truth: AOF everysecond

# Quando usar Redis vs Memcached:
# - Só precisa de cache puro, simples? Memcached é mais leve e simples
# - Precisa de qualquer estrutura de dados além de key-value? Redis
# - Precisa de pub/sub, queue, leaderboard? Redis
# - Precisa de persistência? Redis
# - Operação massivamente multi-threaded sem features avançadas? Memcached escala
#   melhor por core

# Na prática: Redis ganhou em 95% dos casos. Memcached ainda usado em Facebook (escala
# absurda onde simplicidade extrema importa) e em legacy systems.

# CACHE STAMPEDE em Redis cluster: cuidado com slot resharding
# Durante resharding, requests podem ir ao nó errado, ser redirecionadas, retry
# Em cluster grande, isso amplifica carga temporariamente
# Mitigação: client com retry inteligente, e fazer resharding em períodos de baixa

Eviction policies: LRU, LFU, ARC e TinyLFU

Quando o cache enche, qual entry remover? A escolha da política de eviction afeta o hit ratio mais do que a maioria dos engenheiros percebe. LRU é o default em quase tudo, mas não é o melhor em todos os casos — workloads com "one-hit wonders" (chaves acessadas uma única vez) poluem o cache LRU; workloads com chaves "old but popular" sofrem com LFU. Algoritmos modernos (ARC, TinyLFU) tentam o melhor dos dois mundos.

# LRU (Least Recently Used) — o default clássico
# Estrutura: doubly-linked list + hash map
# - Hit: move o entry para o head da lista (mais recente)
# - Miss + cache cheio: remove o tail (menos recentemente usado), insere novo no head
# - Complexidade: O(1) para todas as operações

# Características:
# + Implementação simples, suportada nativamente em Memcached, Redis, browsers, etc
# + Bom para workloads com forte temporal locality (chaves quentes recentes)
# - Problema "scan pollution": uma varredura sequencial (ex: backup, analytics query)
#   poluí o cache com chaves que serão acessadas uma única vez, evictando hot data
# - Não diferencia entre uma chave acessada 1000× e outra acessada 1×

# LFU (Least Frequently Used)
# Conta quantas vezes cada chave foi acessada; evicta a menos acessada
# Estrutura: hash map + min-heap por counter

# Características:
# + Imune a scan pollution (chaves de uso único não acumulam counter alto)
# + Captura "old but consistently used" entries
# - "Cache pollution by old hot items": chaves que foram populares no passado mas
#   não são mais ficam presas (counter alto) e bloqueiam novas chaves populares
# - Counter precisa decair com tempo (LFU-DA, LFU-Aging) para evitar esse problema

# ARC (Adaptive Replacement Cache) — IBM, 2003
# Combina LRU e LFU dinamicamente, adaptando proporção baseado em padrão observado
# Mantém 4 listas:
#   T1: entries recentemente vistos uma única vez
#   T2: entries vistos mais de uma vez
#   B1: entries evictados de T1 (ghost list, só metadata)
#   B2: entries evictados de T2 (ghost list)
# Hit em B1 → ajuste favor recência (aumenta T1)
# Hit em B2 → ajuste favor frequência (aumenta T2)

# Características:
# + Auto-tuning entre recência e frequência sem parâmetro manual
# + Resistência a scan pollution
# + Tipicamente 5-15% melhor hit ratio que LRU em workloads reais
# - Complexidade significativamente maior
# - Patente da IBM expirou em 2024 → mais adoção esperada
# - Usado em ZFS (filesystem), PostgreSQL buffer cache (variante)

# TinyLFU (Window TinyLFU) — Caffeine library, 2015
# State-of-the-art para in-process caches em JVM (e por extensão, Spring, Guava-like)
# Combina LFU com bloom filter probabilístico para tracking de frequência

# Mecanismo:
# 1. Count-Min Sketch (estrutura probabilística): aproxima frequency counters com
#    pouco overhead de memória (kilobytes para track de milhões de keys)
# 2. Window LRU pequeno: novos entries entram aqui primeiro (proteção contra scan)
# 3. Main cache (Segmented LRU): entries do window se "qualificam" via TinyLFU
#    apenas se sua frequency estimada > frequency da chave a ser evictada

# Características:
# + Hit ratio próximo do ótimo teórico (geralmente >LRU em 5-25%)
# + Memory overhead baixo (Count-Min Sketch é compacto)
# + Resistente a scan pollution e cache pollution
# - Implementação complexa (você não escreve isso à mão; usa Caffeine, Ristretto)
# - Caffeine (Java) e Ristretto (Go) implementam TinyLFU corretamente

# Comparação empírica em workload típico (web cache):
# LRU:         baseline (digamos 70% hit ratio)
# LFU:         62% (sofre com cache pollution)
# LFU-Aging:   73% (com decay)
# ARC:         76%
# TinyLFU:     78% (state of the art)
# Belady's OPT: 82% (ótimo teórico, requer conhecer o futuro)

# Recomendação prática:
# - Memcached/Redis: LRU é o default e geralmente suficiente
# - Java in-process: usar Caffeine (TinyLFU); migrar de Guava cache (LRU)
# - Go in-process: usar Ristretto (TinyLFU baseado, do Dgraph team)
# - CDN: provedores usam algoritmos proprietários (geralmente variantes de TinyLFU
#   ou ARC com tuning específico para padrões web)
# - Para workloads previsíveis: às vezes vale calibrar TTL e tamanho do cache antes
#   de trocar de algoritmo — ganho é maior

Cache warming: o problema do cold start

# O problema: você fez deploy de uma nova instância. O cache local está vazio.
# As primeiras requests são todas cache miss → DB sobrecarregado → latência terrível
# Pode demorar minutos até o cache estar "warm" e o sistema voltar ao steady state

# Variantes do problema:
# - Deploy de nova versão do app (instances novas)
# - Failover (instâncias substitutas chegam frias)
# - Restart de Redis cluster (RDB load demorado ou perdido)
# - Cache stampede em escala global (CDN PoP novo entrando em produção)

# ESTRATÉGIA 1: Lazy warming (faz nada)
# Aceitar o cold start; deixar o cache popular naturalmente
# Vantagem: simples, sem código adicional
# Desvantagem: degradação visível para os primeiros minutos de tráfego

# ESTRATÉGIA 2: Proactive warming (script de pre-load)
# Antes de marcar a instance como "healthy" (entrar no load balancer), carregar
# manualmente as N keys mais quentes
# Implementação:
#   - Manter lista de "top hot keys" (analytics histórico, top do dia anterior)
#   - No startup, fazer GET dessas keys → popula cache
#   - Marcar healthy só depois

async def warm_cache_on_startup():
    hot_keys = await analytics.get_top_keys(limit=1000)
    for key in hot_keys:
        await cache_with_db_fallback(key)  # cache miss → DB → populate
    logger.info(f"Warmed {len(hot_keys)} keys")

# Vantagem: instance entra no LB já produtiva
# Desvantagem: aumenta tempo de startup; lista pode não refletir tráfego atual

# ESTRATÉGIA 3: Cache replication (mirror de cache existente)
# Ao subir nova instance, copiar o estado do cache de uma instance saudável
# Redis: BGSAVE no instance source, restore no novo
# Memcached: não suporta nativamente; alguns sistemas usam mcrouter

# Vantagem: cache "wam" instantaneamente
# Desvantagem: requer engenharia; pode mascarar problemas (se source tem cache corrupto)

# ESTRATÉGIA 4: Traffic shaping no load balancer
# Aceitar que cache vai estar frio; reduzir blast radius
# - Slow start: load balancer manda apenas 1-5% do tráfego para nova instance
#   por 60-300s, depois aumenta gradualmente
# - Permite que cache se popule sem destruir o backend

# Suportado por: NGINX, HAProxy, AWS ALB (slow start), envoy proxy

# ESTRATÉGIA 5: Replay de log de tráfego (production traffic shadow)
# Capturar uma amostra do tráfego de produção; replay no startup
# Cache se popula com chaves realmente acessadas, não chute educado

# Implementação:
#   - Sample 1% das requests reais em um log (S3, kafka)
#   - No startup, replay esse log via "shadow" requests (não retornam ao cliente)
#   - Cache popula com tráfego "real"

# Vantagem: cache se aquece com tráfego representativo, não com lista estática
# Desvantagem: complexidade significativa; requer infra de capture/replay

# Para CDN: CDN providers fazem warming automaticamente
# - Cloudflare Argo Tiered Caching: novos PoPs aprendem o que é hot regionalmente
# - Akamai prefetching: pre-fetch baseado em padrões observados
# - Você raramente precisa intervir no warming de CDN; eles cuidam

# A escolha:
# - Para apps pequenas: lazy + slow start no LB resolve 80%
# - Para apps críticas: proactive warming com top keys do dia anterior
# - Para apps em escala extrema: traffic replay vale a complexidade

Bloom filters como negative cache

Bloom filter é uma estrutura probabilística que responde "talvez está no conjunto" ou "garantido que NÃO está". Para caches, isso é poderoso: evitar lookups no DB para chaves que sabidamente não existem. Comum em scenarios onde clientes pedem chaves inválidas (bots, web scraping, IDs antigos) — o "negative cache" protege o DB sem precisar armazenar a lista completa de "não existe".

# O problema clássico:
# Usuário (ou bot) pede /products/12345678 — não existe
# Cache miss em todas camadas → vai ao DB → DB confirma "não existe"
# Próxima request mesma key: novamente todas camadas miss → DB de novo
# Bots podem fazer milhões dessas requests; DB sofre com queries inúteis

# SOLUÇÃO NAIVE: cachear "not_found" também
cache.set("product:12345678", "NOT_FOUND", ttl=300)
# Funciona, mas: 10M chaves inexistentes cacheadas = 10M entries no cache
# Pode encher o cache, evictar chaves legítimas, etc

# SOLUÇÃO ELEGANTE: Bloom filter
# Estrutura probabilística:
# - Bit array de M bits
# - K funções de hash
# - ADD(item): aplica K hashes, seta os bits correspondentes
# - QUERY(item): aplica K hashes, verifica se TODOS os bits estão setados
#   - Todos setados: PROVAVELMENTE no conjunto (false positive possível)
#   - Algum não setado: GARANTIDAMENTE não no conjunto (zero false negatives)

# Math:
# - M = 10 × N onde N = elementos esperados → false positive rate ~1%
# - M = 14 × N → ~0.1%
# - Memory: muito menor que armazenar os elementos (ex: 1M elementos = 1.2MB)

# Aplicação como negative cache:
# Manter um bloom filter de TODAS as chaves que EXISTEM no DB
# Antes de consultar o DB para um cache miss, verificar o filter:
# - Filter diz "não existe": retorna 404 imediatamente, sem tocar o DB
# - Filter diz "talvez existe": consulta o DB (false positive aceitável)

# Pseudocódigo:
async def get_product(product_id):
    # 1. Cache lookup (Redis ou outro)
    cached = await cache.get(f"product:{product_id}")
    if cached:
        return cached if cached != "NULL" else None

    # 2. Bloom filter (in-memory, copia mais recente carregada do Redis)
    if not bloom_filter.might_contain(product_id):
        # Garantido NÃO existe — não consulta DB
        return None

    # 3. DB query (apenas se filter disse "talvez")
    product = await db.query("SELECT * FROM products WHERE id = ?", product_id)
    if product:
        await cache.set(f"product:{product_id}", product, ttl=300)
    else:
        # False positive do filter — cacheamos "não existe" curto
        await cache.set(f"product:{product_id}", "NULL", ttl=60)
    return product

# Manutenção do bloom filter:
# - Quando insere novo product no DB: bloom_filter.add(product_id)
# - Quando deleta product: PROBLEMA — bloom filter clássico não suporta remoção
#   - Soluções: Counting Bloom Filter (cada bit vira counter), ou rebuild periódico
# - Para reads-mostly com poucos deletes: rebuild diário/horário do filter completo
# - Redis tem módulo RedisBloom com BF.RESERVE, BF.ADD, BF.EXISTS

# Casos reais de uso:
# - Bitcoin SPV: clientes têm bloom filter dos seus endereços; servidor envia apenas
#   transações que podem matchar (privacy + bandwidth)
# - Google Chrome Safe Browsing: filter de URLs maliciosas embutido no browser
# - Cassandra: bloom filter por SSTable para evitar disk seek em lookups que não existem
# - Cache CDN: alguns CDNs usam bloom filter para detectar "definitely not cached"

# Quando NÃO usar:
# - Sets pequenos (<10k itens): hash set normal é mais simples e exato
# - Quando false positives são caros (cada lookup ao DB já é cheio): vale ter exatidão
# - Quando deletes são frequentes: bloom filter clássico não remove; alternativas adicionam
#   complexidade que muitas vezes não vale

TLS termination no edge

# Onde o TLS handshake acontece importa para latência e arquitetura

# TLS handshake custa caro:
# - 2-RTT para TLS 1.2 (até 4 roundtrips totais com TCP)
# - 1-RTT para TLS 1.3 (após primeiro contato; 0-RTT possível para reconexão)
# - Para usuário em Singapura conectando origin em Virginia (200ms RTT):
#   TLS 1.2: ~600ms só para handshake; TLS 1.3: ~400ms (e isso ANTES do primeiro byte)

# CDN com TLS termination no edge resolve isso:
# 1. User TCP/TLS handshake → PoP local (Cloudflare em São Paulo, ~10ms RTT)
# 2. PoP mantém conexão TLS WARM com origin (connection pooling, multiplexing)
# 3. Request do user é enviada pela conexão TLS já estabelecida com origin
# 4. Response volta pelo mesmo pipe TLS, criptografada novamente para o user

# Latência total: ~30ms TLS local + 1× request body cross-continent = ~250ms
# Vs sem CDN: ~600ms só de handshake + request

# Benefícios adicionais:
# - DDoS mitigation antes do origin: PoP filtra ataques de SYN flood, slowloris
# - Certificate management centralizado: cert no PoP, origin pode usar cert interno
# - Modern protocols sem upgrade do origin: PoP fala HTTP/3 com user mesmo se origin
#   só fala HTTP/1.1

# DOIS MODELOS de TLS na CDN:

# 1. Full TLS (recomendado):
# User <--TLS--> CDN edge <--TLS--> Origin
# Conexão entre CDN e origin é criptografada também
# Necessário: certificado no origin (Let's Encrypt, comercial, ou cert da CDN)
# Cloudflare oferece "Origin CA Certificates" — cert válido apenas para CF→origin

# 2. Flexible TLS (NÃO use em produção):
# User <--TLS--> CDN edge <--plain HTTP--> Origin
# Tráfego User→CDN é encriptado; CDN→Origin é HTTP plain
# Risco: man-in-the-middle entre CDN e origin (rede AWS/cloud é segura mas...)
# Uso aceitável: dev/staging com origin atrás de VPN/private network apenas

# mTLS (mutual TLS) origin: para extra segurança
# Origin valida que o caller é a CDN (não alguém pulando a CDN direto)
# - Cloudflare Authenticated Origin Pulls: cliente cert obrigatório
# - Verifica: apenas Cloudflare pode chamar o origin
# - Protege contra: atacantes descobrindo IP do origin e contornando a CDN

# TLS 1.3 + 0-RTT (zero roundtrip time):
# Reconexões: cliente já sabe parâmetros da sessão; pode enviar request junto com handshake
# Latência percebida: 1 RTT economizado em reconexão
# Risco: replay attacks (request 0-RTT pode ser reenviada por atacante)
# Solução: aplicar 0-RTT apenas para requests idempotentes (GET sem efeitos)

# QUIC + HTTP/3 = TLS embarcado
# HTTP/3 (HTTP sobre QUIC sobre UDP) tem TLS 1.3 nativo no transport
# Vantagens: faster handshake, immune a head-of-line blocking, melhor em redes ruins
# Adoção: ~30% do tráfego web em 2026 (Google, Facebook, Cloudflare suportam totalmente)

# Custo do TLS no edge:
# - CPU para encryption/decryption (mitigado com AES-NI hardware, ChaCha20 em ARM)
# - Memória para session caching
# - Certificate management ops
# Provider lida com tudo isso; usuário só paga taxa fixa da CDN

Arquitetura completa

USER (browser/mobile app)
  │
  │ DNS lookup → anycast IP da CDN
  ▼
┌──────────────────────────────────────┐
│  CDN EDGE (PoP mais próximo)         │
│  - Cache check (URL + headers)       │
│  - DDoS mitigation                   │
│  - WAF (firewall L7)                 │
│  - Edge functions (Workers)          │
└──────────────┬───────────────────────┘
               │ cache miss
               ▼
┌──────────────────────────────────────┐
│  ORIGIN SHIELD (regional)            │
│  - Aggregates misses from 50+ edges  │
│  - Reduces origin load 10-100x       │
└──────────────┬───────────────────────┘
               │ cache miss
               ▼
┌──────────────────────────────────────┐
│  LOAD BALANCER (origin)              │
└──────────────┬───────────────────────┘
               │
               ▼
┌──────────────────────────────────────┐
│  APPLICATION TIER                    │
│  ┌────────────────────────────────┐ │
│  │ In-process cache (per instance)│ │
│  │  - LRU, 100MB-1GB              │ │
│  │  - Caffeine (Java), Memcached  │ │
│  │    embedded, MemoryCache (.NET)│ │
│  └─────────────┬──────────────────┘ │
│                │ miss                │
│                ▼                     │
│  ┌────────────────────────────────┐ │
│  │ Distributed cache (Redis cluster) │
│  │  - Shared across instances     │ │
│  │  - 50ms TTL para hot data      │ │
│  │  - Cache-aside pattern         │ │
│  └─────────────┬──────────────────┘ │
│                │ miss                │
│                ▼                     │
│  ┌────────────────────────────────┐ │
│  │ Database                       │ │
│  │  - Postgres / Cassandra / etc  │ │
│  └────────────────────────────────┘ │
└──────────────────────────────────────┘

INVALIDAÇÃO (paralelo ao fluxo de read):

DB write
  │ + outbox event
  ▼
Kafka topic (data.changes)
  │
  ▼
Cache Invalidation Worker
  ├─→ Redis: DEL keys relacionadas
  ├─→ CDN API: purge URLs relacionadas
  └─→ In-process caches: pub/sub broadcast para limpar local

OBSERVABILIDADE crítica:
  - Hit ratio por camada (browser, CDN edge, Redis, in-process)
  - Latency P50/P99 por camada
  - Origin load (requests/s que chegaram até o backend)
  - Cache stampede events (multiple misses for same key in window)
  - Invalidation latency (write → cache invalidated em todas camadas)
  - Memory usage e eviction rate por cache layer
o tradeoff que nunca desaparece: hit ratio vs freshness

Toda decisão de cache é um ponto num espectro entre "máxima freshness" (TTL=0, sem cache, sempre lê do origin — slow mas sempre correto) e "máximo hit ratio" (TTL=infinito, immutable, nunca invalida — fast mas pode servir dados anos antigos). Não existe ponto ótimo universal; cada operação tem o seu. O sintoma de cache mal dimensionado é sempre o mesmo padrão: usuário reclama "atualizei mas não vejo a mudança" (TTL longo demais) OU origin está sobrecarregado em pico (TTL curto demais, hit ratio baixo). Engenheiros maduros pensam em cache não como "ligar ou desligar" mas como "qual é o trade-off correto para esta operação específica?". Os artefatos arquiteturais que ajudam: dashboards de hit ratio por endpoint, alertas em origin load, e revisões periódicas de TTL conforme o produto evolui — cache que foi bem dimensionado há 6 meses pode estar mal dimensionado agora.

Decisões de engenharia

CDN: usar uma global vs multi-CDN vs própria

CDN global (Cloudflare, CloudFront, Akamai, Fastly) é o default razoável: setup em horas, cobertura mundial, custo previsível (~$0.08/GB para primeiros TB, escala bem com volume). Multi-CDN é a próxima evolução para sistemas onde uptime importa demais — usar 2+ CDNs com balanceamento DNS, evita single point of failure (Cloudflare teve outage global em 2022 que tirou Discord, Shopify, e centenas de outros sites simultaneamente). Própria CDN só faz sentido em escala extrema (Netflix, YouTube, Spotify) onde o custo de alugar e o volume justificam infraestrutura dedicada — e essas empresas têm times de 50+ engenheiros operando suas CDNs.

Regra prática: 99% dos sistemas → uma CDN comercial. Cloudflare é o default popular (preço bom, features amplas, generous free tier). CloudFront se você é all-in na AWS. Akamai para enterprise tradicional. Fastly para times técnicos avançados que querem mais controle. Multi-CDN só quando uptime é literalmente parte do contrato — geralmente fintech, gaming, e infrastructure services.

Cache-aside vs write-through

Cache-aside (a aplicação gerencia: lê cache, miss → lê DB e popula cache; escreve → invalida cache) é o padrão dominante porque é resiliente: se cache cai, aplicação degrada mas continua funcionando. Write-through (escreve cache e DB juntos) é mais simples mentalmente — cache sempre fresh — mas tem dois problemas: latência de write inclui ambos sistemas, e atomicidade entre cache e DB é difícil (se DB falha após cache update, inconsistente).

Regra prática: cache-aside por default para 90% dos casos. Write-through faz sentido quando: reads são absurdamente mais frequentes que writes (write em ambos não custa, e elimina cache miss), ou quando o cache library oferece transação distribuída confiável (raro). Para cache distribuído com Redis, cache-aside é praticamente sempre o caminho. Atenção: a ordem importa — fazer DB write primeiro, depois invalidar cache; nunca o contrário (race condition pode reintroduzir valor antigo entre invalidação e atualização do DB).

TTL longo + purge explícito vs TTL curto sem purge

TTL longo (horas ou dias) + purge quando o conteúdo muda dá o melhor de ambos os mundos teóricos: máximo hit ratio + freshness imediata. Mas operacionalmente, a lógica de purge fica espalhada (cada path de write precisa saber o que invalidar), purges em escala são caros (milhões de chaves para invalidar), e falhas em purge passam silenciosamente. TTL curto (segundos a minutos) sem purge é operacionalmente mais simples — você aceita alguns segundos de staleness, e não há código complexo de invalidação.

Regra prática: TTL curto (1-5min) para 80% dos casos onde "alguns minutos atrasado" é OK. TTL longo + purge para conteúdo crítico onde freshness imediata importa e o número de keys a invalidar é gerenciável (ex: pagina de produto que precisa refletir mudança de preço imediatamente). Surrogate keys (Fastly, Varnish) ajudam quando a invalidação é semântica ("invalida tudo do produto X") em vez de URL-by-URL.

In-process cache vs Redis distribuído

In-process cache (Caffeine, Guava, MemoryCache) é trivial e absurdamente rápido (sub-microssegundo, sem rede), mas tem três problemas: cada instância tem cache separado (memória multiplicada), invalidação cross-instance é difícil (precisa pub/sub), e cold-start de novas instâncias é caro. Redis distribuído resolve esses problemas — cache compartilhado, invalidação centralizada — mas adiciona latência de rede (~1-2ms LAN) e cria dependência operacional.

Regra prática: usar AMBOS em camadas. In-process cache (1-5min TTL) para hot data que muda raramente — protege a maior parte das requests do roundtrip de Redis. Redis (TTL maior) como segunda camada, compartilhada. DB como última camada. Invalidação via pub/sub para in-process (Redis broadcast) + DELETE direto em Redis. Esse padrão de 3 camadas (in-process → Redis → DB) é o que sistemas maduros usam — cada camada protege a próxima e tem características complementares.

Perguntas de entrevista

Você adicionou CDN ao seu site e o hit ratio é de 30%. Como você investiga e melhora?

30% de hit ratio em CDN é baixo — sistemas bem configurados ficam em 80-95% para conteúdo estático e 50-70% para dinâmico cacheável. A investigação tem várias frentes.

1. Verificar headers de cache: a primeira coisa. Olhar response headers do origin para diferentes tipos de conteúdo. Se está mandando Cache-Control: no-cache ou private em assets que deveriam ser cached, a CDN está fazendo o que foi pedido (não cachear). Fix: configurar origin para enviar Cache-Control adequado, ou override na CDN.

2. Fragmentação por query strings: CDNs por default consideram query string parte da cache key. Se URLs têm tracking parameters (utm_source, fbclid, gclid) variados, cada combinação vira chave separada. Mesmo conteúdo, mil cache keys diferentes. Fix: configurar CDN para ignorar tracking parameters específicos (Cloudflare: "Cache by Query String" → ignore these).

3. Vary headers excessivos: verificar se origin manda Vary: User-Agent, Vary: Cookie ou similar. Cada User-Agent único = entry separada no cache. Bilhões de User-Agents reais existem (combinações de browser version + OS version + extensions). Fix: remover Vary headers desnecessários; usar Vary apenas para Accept-Encoding (raramente Accept-Language).

4. Conteúdo dinâmico não-cacheable: talvez 70% das URLs são genuinamente dinâmicas (user-specific). Nesse caso, hit ratio de 30% pode ser correto — você está cacheando apenas o subset cacheável. Verificar: separar métricas por tipo de URL. Conteúdo estático deveria ter 90%+, APIs públicas 50%+, user pages 0% (correto).

5. TTLs curtos demais: se max-age=60 e o conteúdo raramente muda, TTL maior aumentaria hit ratio. Verificar: é razoável estender? Para assets versionados (com hash no filename): sim, immutable + max-age=1year. Para HTML: depende do produto.

6. PoPs mal distribuídos: se o tráfego vem de regiões onde a CDN não tem PoPs próximos, requests vão para PoPs distantes e cada um tem cache separado. Fix: escolher CDN com presença forte nas regiões dos seus usuários, ou habilitar features de "tiered caching" que consolidam misses regionalmente.

7. Conteúdo grande sendo evicted: CDN PoPs têm capacidade finita; assets enormes (vídeos, datasets) podem ser evicted rapidamente se há muita variedade de conteúdo. Fix: usar cold storage (S3 + CDN otimizada para video) para conteúdo grande pouco acessado.

A métrica isolada não é diagnóstico — segmentar por endpoint, tipo de conteúdo, status code, e geo é essencial.

Como evitar que uma única chave popular expirando ao mesmo tempo derrube o seu DB?

Esse é o problema clássico de cache stampede (também chamado de thundering herd) — uma das causas mais frequentes de outage em sistemas que dependem de cache. Várias estratégias, podem ser combinadas:

1. Stale-while-revalidate: a solução mais elegante quando suportada. Configurar Cache-Control: max-age=300, stale-while-revalidate=60. Por 5 minutos: serve fresh. Entre 5 e 6 minutos: serve stale, dispara refresh em background. Após 6 minutos: bloqueia para refresh sync. Apenas o primeiro request "perde" a janela; todos os outros recebem o valor stale enquanto o refresh acontece. Suportado nativamente em Cloudflare, Fastly, Vercel, browsers modernos.

2. Probabilistic early refresh: em vez de TTL absoluto, atualizar probabilisticamente perto do expiry. Implementação simples: probabilidade de refresh = (time_since_creation / ttl)^N. Quando T = 0, probability = 0. Quando T = TTL, probability = 1. Apenas alguns requests refresh nos últimos segundos; a maioria continua usando cached. Sem stampede no momento exato do expiry.

3. Single-flight (request coalescing): dentro de um processo, apenas UM request por chave faz o loader; outros aguardam o resultado. Implementação trivial em Go (singleflight package), em Python via asyncio.Future. Para múltiplos processos: usar Redis SETNX como lock, com TTL curto para evitar deadlock.

4. TTL jitter (randomização): se você popula muitas chaves ao mesmo tempo (warmup ou bulk load), elas vão expirar juntas. Adicionar ±20% aleatório ao TTL espalha as expirações. Simples, efetivo, sem custo. Fórmula: ttl = base * (1 + random(-0.2, 0.2)).

5. Refresh-ahead (background refresh): para chaves muito quentes, ter um job background que atualiza o cache antes do expiry, sem aguardar request. Mais complexo (precisa saber quais chaves são "hot enough" para justificar), mas elimina cache miss completamente para essas chaves.

6. Lock distribuído explícito: antes de chamar o loader, tentar adquirir lock no Redis (SET NX EX 30). Se ganhou: faz o loader, popula cache, libera lock. Se perdeu: pollar cache por alguns segundos esperando o outro processo terminar. Funciona mas é mais código que single-flight in-process.

Na prática: começar com stale-while-revalidate (se a CDN/cache suporta) + TTL jitter. Adicionar single-flight no app server. Para chaves super-hot, considerar refresh-ahead. Lock distribuído como último recurso quando outras estratégias não bastam.

Como você implementa "invalida tudo do usuário X" eficientemente quando o cache tem chaves espalhadas em várias camadas?

Esse é o problema de invalidação semântica — em vez de invalidar URL-by-URL, invalidar tudo relacionado a uma entidade. As soluções variam por camada.

1. Surrogate keys / tags (na CDN): Fastly e Varnish suportam nativamente. Cada response recebe tags via header Surrogate-Key: user-123 profile-data. Para invalidar: chamar API com a tag, todas as URLs com aquela tag são purged. Cloudflare tem Cache Tags Enterprise (similar). Esse é o cleanup ideal: 1 call para invalidar 100s de URLs relacionadas.

2. Convenção de naming + SCAN no Redis: nomear chaves com prefixo da entidade. user:123:profile, user:123:posts, user:123:settings. Para invalidar: SCAN cursor MATCH user:123:* COUNT 100, então DELETE batch. CUIDADO: SCAN não bloqueia mas é O(N) total — em DB grande, pode demorar segundos. Para chaves volumosas, usar SCAN em chunks com pequena pausa entre eles.

3. Reverse index de chaves: manter user:123:cache_keys como um SET de chaves que pertencem ao usuário. Ao popular cache, adicionar a chave ao SET. Ao invalidar: SMEMBERS user:123:cache_keys + DELETE batch. Mais explícito que SCAN, sem custo de varredura, mas requer disciplina de adicionar ao SET sempre.

4. Event-driven em camadas: emitir evento user.invalidated(123) em Kafka. Consumidores em paralelo: (a) chama CDN purge para URLs do user; (b) deleta keys no Redis; (c) pub/sub para in-process caches limparem. Mais robust (resiliente a falhas parciais), mas latência maior (segundos).

5. Versioning por entidade: em vez de invalidar, mudar a versão. Manter user:123:version = N. Cache keys: user:123:v{N}:profile. Para "invalidar": INCR user:123:version — todas as chaves antigas viram orfãs e expiram por TTL natural. Sem deletar nada; cache hit ratio cai temporariamente até repopulação. Elegante mas desperdiça memória (chaves órfãs ficam até TTL).

6. Em camada de CDN com tags: Fastly purge com surrogate key é O(1) operacionalmente, mas a propagação para todos os PoPs leva ~150ms a alguns segundos. Soft purge (não remove imediatamente, marca como stale) é mais rápido e mais barato.

A escolha depende da escala: para apps pequenas, naming convention + DEL batch funciona. Para apps com milhões de usuários, event-driven com Kafka + workers paralelos é mais robust. Para apps com latência crítica de invalidação, surrogate keys na CDN são insubstituíveis.

Em que casos você NÃO usaria cache, mesmo que tecnicamente pudesse?

Cache parece sempre uma boa ideia, mas há casos onde adicioná-lo é estritamente prejudicial — entender quando não usar é tão importante quanto entender quando usar.

1. Quando freshness é parte do contrato com o usuário: sistemas financeiros (saldo bancário, executions de ordem), estoque crítico (último item disponível), real-time scoring. Mesmo "5 segundos atrás" pode ser inaceitável. Solução: bypass cache, leitura direta do DB com replica de leitura quando possível.

2. Quando o conteúdo é altamente personalizado e o hit ratio seria ~0: dashboards específicos do usuário, feeds personalizados ranqueados por ML, carrinhos de compra. Cada usuário tem visão única; cache fragmenta sem ganho. Solução: cachear partes compartilháveis (template HTML, listas referenciadas) e renderizar a parte personalizada sempre.

3. Quando o conteúdo é gerado por API call externa cara COM rate limit baixo: aqui parece "óbvio cachear", mas o tradeoff é sutil. Se o rate limit é estrito (ex: 10 req/h), cache TTL longo pode mascarar uma mudança crítica do provedor externo. Solução: cachear, mas combinar com webhook do provedor para invalidação imediata.

4. Quando o custo de invalidação é maior que o ganho: se você precisa invalidar 1M chaves a cada mudança e a chave é raramente lida, custa mais do que economiza. Solução: não cachear, ou cachear com TTL curto sem invalidação ativa.

5. Quando cache mascara problemas reais do sistema: cache que esconde uma query lenta no DB pode aliviar o sintoma mas não resolver a causa. Quando o cache miss acontece (deploy, falha de cache), o sistema cai. Solução: monitorar latência sem cache (do origin); manter o sistema funcional mesmo com cache cold.

6. Quando dados são sensíveis e shared cache é risco: dados pessoais ou financeiros num CDN podem vazar para outros usuários se cache key estiver mal configurado (já aconteceu com Apollo, Snap, etc — dados de um usuário aparecendo para outro). Solução: Cache-Control: private, no-store para qualquer endpoint com dados sensíveis.

7. Quando o sistema é write-heavy e read-cold: se a maioria das chaves é escrita uma vez e raramente lida, popular cache é desperdício. LRU já vai evictá-las antes de serem reaproveitadas. Solução: não cachear, ou cache TTL muito curto para o read raro que acontece logo após write.

A regra geral: cache é uma otimização. Otimizações têm custos (memória, complexidade, casos de borda). Se o problema que cache resolve não é demonstrado, a melhor otimização é não adicionar cache.

Como você projeta o cache de uma página de produto de e-commerce com milhões de SKUs?

Página de produto é um clássico problema multi-camada de cache, com requisitos contraditórios: deve ser fast (latência sub-100ms global), deve refletir mudanças (preço, estoque) rapidamente, e deve escalar para milhões de SKUs com longa cauda.

Análise da página: a página tem várias partes com características diferentes:

(a) Layout/template HTML: muda raramente (deploys); pode cache infinito com cache busting (immutable + content hash). CDN edge cache.

(b) Dados estáticos do produto: nome, descrição, imagens, especificações. Mudam ocasionalmente; TTL de 1 hora aceitável. CDN edge cache com tag por product_id.

(c) Preço: muda frequentemente (promoções, dynamic pricing); TTL curto (5 min) + purge quando muda. Cache em Redis, não CDN (varia por região, faixa de usuário).

(d) Estoque: muda em tempo real; cache muito curto (10s) ou sem cache. Direto do DB com lock distribuído para evitar overselling.

(e) Recommendations (clientes que compraram X também...): pode ser cached por categoria + product_id; TTL 1h é OK.

(f) Reviews: lista pode ser cached (5min); contador deve estar fresh se possível.

Arquitetura proposta:

1. CDN edge: serve o HTML/template, e os dados estáticos do produto. Cache TTL 1h com surrogate key product:{id}. Invalidação via purge quando product muda. Hit ratio esperado: 90%+ para produtos populares.

2. Edge compute (Cloudflare Workers): personalizes a página sem buster cache — inject preço dinâmico via JavaScript no edge, baseado em geo do usuário, A/B test variant, ou logged-in status.

3. Origin Shield: reduz origin load. Quando edges têm cache miss, vão para shield primeiro. Para hot products, talvez 95% dos misses são absorvidos no shield.

4. Redis cluster (no origin): cacheia preço (TTL 5min), recommendations (TTL 1h), reviews count (TTL 5min). Cache-aside com purge on update.

5. In-process cache (LRU): nos app servers, cachear dados de produto que vêm do DB (TTL 30s). Reduz roundtrip ao Redis para hot products.

6. DB: Postgres ou DynamoDB com leitura otimizada por product_id. Reads de longo tail (produtos raramente vistos) batem aqui.

Estoque (caso especial): não cachear; ler direto do DB. Se latência é problema, manter um contador atômico em Redis sincronizado com DB via eventual consistency, mas validar com DB antes do checkout (segunda checagem).

Invalidação coordenada: quando admin atualiza um produto, evento Kafka product.updated(id, fields). Workers em paralelo: (a) CDN purge com surrogate key; (b) DEL keys no Redis; (c) pub/sub para in-process caches; (d) update ElasticSearch index para search.

Observability: hit ratio por camada e por tipo de dado; latência P99 por endpoint; origin load por tipo de cache miss; alertas em outage de Redis (deve degradar graciosamente, não cair).

O resultado: 90% das pageviews servidas em <100ms do edge; 9.9% servidas em <200ms do origin + cache; 0.1% (cold products) em ~500ms da DB. Sistema escala para milhões de SKUs sem origin saturar.

Exercícios práticos

Exercício 1 — Implementar cache-aside com TTL

Implemente um cache in-memory (dict + timestamp por chave) com cache-aside pattern. Função get(key, loader): se cache hit e não expirado, retorna; se miss ou expirado, chama loader, popula cache. Testar com 1000 chaves, simular loader que demora 100ms, medir latência: primeira chamada lenta (cache miss), chamadas subsequentes rápidas (cache hit). Implementar também eviction LRU quando cache atinge 100 chaves.

Critério: cache hit ratio >90% após warmup com chaves aleatórias dentro de set de 100. Latência média <1ms para hits, ~100ms para misses. Eviction LRU funcional: chave menos recentemente acessada é removida quando cache enche. Bonus: adicionar TTL jitter (±20%) para evitar expiração simultânea de chaves populadas juntas.

Exercício 2 — Single-flight para prevenir cache stampede

Estenda o Exercício 1 com single-flight: se múltiplos requests para a mesma chave chegam simultaneamente em cache miss, apenas UM chama o loader; os outros aguardam o mesmo resultado. Usar asyncio.Future (Python) ou similar. Simular: 100 requests concorrentes para chave A em cache miss; o loader (que conta chamadas) deve ser chamado APENAS UMA VEZ.

Critério: com 100 requests concorrentes para mesma chave em miss, loader é chamado exatamente 1 vez. Todos os 100 requests recebem o mesmo valor. Latência do loader é repassada (todos esperam 100ms se loader demora 100ms). Após cache populado, requests subsequentes são fast. Bonus: implementar lock distribuído com Redis SETNX para coordenar single-flight entre múltiplos processos.

Exercício 3 — Cache de 3 camadas (in-process + Redis + DB)

Configure: SQLite/Postgres com 10k produtos; Redis local; in-process cache (LRU 100 entries). Implementar get_product(id) que: (1) verifica in-process cache (TTL 30s); (2) miss → verifica Redis (TTL 5min); (3) miss → consulta DB e popula ambas as camadas superiores. Testar com 10k requests aleatórios e medir hit ratio em cada camada, e latência média.

Critério: in-process hit ratio >60% para hot products (200 produtos mais acessados). Redis hit ratio >90% incluindo in-process misses. DB load <5% do total. Latência média: in-process <0.1ms, Redis <2ms, DB <20ms. Implementar invalidação multi-camada: ao update produto no DB, deletar no Redis E em todos os in-process (pub/sub broadcast).

Exercício 4 — Stale-while-revalidate com background refresh

Implemente cache com SWR: além de max-age (TTL), permitir stale-while-revalidate window. Após max-age: serve o valor stale e dispara refresh em background (sem aguardar). Após stale window: bloqueia para refresh sync. Testar: chave com max-age=10s, swr=60s. Request aos 5s: cache hit fresh. Aos 15s: cache hit stale + background refresh. Aos 80s: bloqueia para refresh.

Critério: requests entre max-age e max-age+swr retornam stale value rapidamente (não esperam refresh). Background refresh atualiza o cache para próximas requests. Requests após o swr window bloqueiam até refresh. Mensurar: latência de request em stale window é <5ms (independente do tempo do loader). Loader é chamado apenas 1 vez por janela mesmo com requests concorrentes.

Exercício 5 — Versioned cache keys (sem invalidação ativa)

Implemente um cache com versioning: cada entidade tem uma versão atual (counter no Redis). Chaves de cache incluem a versão: user:{id}:v{version}:profile. Para invalidar tudo de um usuário: INCR user:{id}:version. Chaves antigas viram orfãs (não são deletadas; expiram por TTL). Testar com 1000 usuários e operações de read/write — verificar que após "invalidação" (incr version), novas leituras populam novas chaves e antigas expiram naturalmente.

Critério: após INCR version, primeira read do user é cache miss (chave nova) e popula. Reads subsequentes são cache hit na nova versão. Chaves antigas continuam no Redis (verificar com KEYS user:123:v*) mas não são lidas. Após TTL, chaves antigas somem. Comparar uso de memória vs cache com invalidação ativa para mesma carga — versioning desperdiça mais memória temporariamente mas é operacionalmente mais simples.

Referências

  1. standard RFC 7234 — Hypertext Transfer Protocol (HTTP/1.1): Caching datatracker.ietf.org/doc/html/rfc7234 · o spec definitivo de cache HTTP — Cache-Control, ETags, Vary, conditional requests. Leitura densa mas essencial para entender headers profundamente
  2. standard RFC 5861 — HTTP Cache-Control Extensions for Stale Content datatracker.ietf.org/doc/html/rfc5861 · spec do stale-while-revalidate e stale-if-error — a solução elegante para cache stampede e degradation graceful
  3. docs Cloudflare — How Cloudflare's CDN Works developers.cloudflare.com/cache/ · documentação detalhada da CDN da Cloudflare — cache behavior, configuração, edge functions (Workers), e debugging
  4. docs Fastly — Surrogate Keys and Cache Tags docs.fastly.com/en/guides/working-with-surrogate-keys · o modelo de invalidação semântica do Fastly — referência para qualquer sistema que precisa purge por entidade, não por URL
  5. article Vixie, P. — Rate-limiting State (sobre stale-while-revalidate) queue.acm.org · paper conceitual de Paul Vixie (autor do BIND) sobre por que stale-while-revalidate é a estratégia de cache correta para a internet escalável
  6. paper Vattani, A. — Optimal Probabilistic Cache Stampede Prevention VLDB · 2015 · o paper que formalizou matematicamente a estratégia de probabilistic early refresh — base para implementações modernas anti-stampede
  7. article Facebook Engineering — Scaling Memcache at Facebook research.facebook.com · paper clássico sobre como o Facebook escalou Memcached para serve trilhões de requests/dia — lease tokens, gutter cache, regional pools
  8. article Netflix Tech Blog — EVCache: Distributed In-Memory Caching netflixtechblog.com · arquitetura do EVCache da Netflix — fork especializado do Memcached, replicação cross-region, e como suportam dezenas de milhões de ops/s
  9. docs Redis — Distributed Locks with the Redlock Algorithm redis.io/docs/manual/patterns/distributed-locks/ · o algoritmo Redlock para locks distribuídos — útil para coordenar single-flight e cache refresh em múltiplos processos
  10. article Kingsbury, K. — How to Lose Data in Distributed Caches aphyr.com · análise crítica de como caches distribuídos podem perder ou corromper dados — leitura humilde para quem trata cache como infalível
  11. book Hartmut, K. — High Performance Browser Networking O'Reilly · 2013, ainda válido · capítulos sobre HTTP caching, HTTP/2 server push, e otimização de delivery web. Acessível mas profundo
  12. article Akamai — The State of the Internet Report akamai.com/our-thinking/the-state-of-the-internet · relatórios trimestrais com dados reais de latência global, adoção de CDN, attack patterns — informa decisões de arquitetura