Caching
O template usa caching em camadas para performance e resiliência no Cloudflare Workers.
Para a visão sistêmica de "como tudo se encaixa" (incluindo front-service-api e o browser), veja Arquitetura > Cache architecture.
Para "como invalidar X em produção", veja Infraestrutura > Cache operations.
:::info Duas coisas que definem o comportamento hoje
api-clientserver-side está desabilitado no brand worker viaserverCache.delegateCaching: true. A única camada de cache de dados de API dentro do worker é o platform-cache engine — fim da duplicação que causava postmortems de staleness (/payment-providers,/appearance).- A camada
api:foi REMOVIDA (2026-07). O cache de resposta crua do BFF para/api/*não existe mais no brand worker: todo/api/*proxia via service bindingAPI_SERVICEe ofront-service-apié a única fonte de cache raw-BFF. Cachear nos dois lugares era redundante e causava staleness por conflito de TTL. :::
Camadas de cache
Requisição
|
v
[0] Native Workers Caching (RFC 9111) — EXPERIMENTAL, ver seção abaixo
| Cloudflare consulta o cache ANTES de invocar o worker
v
[1] Brand Worker Middleware — SSR HTML/.data
| CF Cache API `ssr:${BUILD_ID}` / `ssr-auth:${BUILD_ID}` + KV
| Anônimo: HTML 60s + SWR 120s · .data 60s + SWR 120s · KV 180s
| Logado (por sessão): HTML 60s · .data 30s · KV 600s
v
[2] platform-cache engine — dados lógicos (memory → CF Cache API → KV snapshot)
| Policy: PLATFORM_CACHE_POLICY_JSON (do front-ops)
| Tier 2 namespace: `platform-cache-<CACHE_GENERATION>`
v
[3] front-service-api proxy worker (Smart Placement) — via binding API_SERVICE
| CF Cache API + KV. TTLs por path prefix (do front-ops)
v
[4] BFF
O browser tem os seus próprios caches (HTTP + Service Worker) — ver a seção "Camada 4 — caches do browser" abaixo.
Cache generation — qual valor vale
O token aparece em quatro lugares e é importante saber qual ganha — só o primeiro é operacional:
| Fonte | Valor | Papel |
|---|---|---|
front-ops/config/cache/generation.yml (current:) | rotacionado com frequência | Fonte operacional de verdade. O workflow bump-cache-generation incrementa e o deploy injeta como CACHE_GENERATION no env do worker |
core/packages/platform-cache/src/generation.ts (DEFAULT_CACHE_GENERATION) | apcsrasj-v3 | Fallback in-source, usado só quando nenhuma env var é injetada |
workers/middleware.ts (resolveGeneration) e app/services/platform-cache.server.ts | apcsrasj-v5 | Fallback in-source do próprio base, mesma condição |
public/sw.js (CACHE_NAME) | app-apcsrasj-v6-<BUILD_ID> | Prefixo do cache do Service Worker (constante hardcoded, não lê env) |
Regra prática: em qualquer ambiente deployado, o valor efetivo é o injetado a partir do
generation.yml. Os defaults in-source só aparecem em dev local ou quando a var não foi injetada
— não são o número "atual". Não pine um número em documentação nem em código novo: consulte
generation.yml.
CACHE_GENERATION é a rotação sob demanda ("nuke from orbit"). Não confundir com o BUILD_ID,
que rota a cada deploy automaticamente.
Camada 0 — Native Workers Caching (experimental)
:::warning Experimento, não comportamento assentado
O wrangler.toml habilita [cache] enabled = true, mas o próprio arquivo enquadra isso como
experimento a validar — o comentário termina com "Testar num STAGE primeiro. Avaliar migrar OFF
do cache manual em vez de rodar os dois." Trate esta seção como descrição do que está
configurado, não como o modelo final. A decisão de manter ou remover precisa do owner de
infraestrutura.
:::
O native caching (RFC 9111) faz a Cloudflare consultar o cache antes de invocar o worker, com
base no Cache-Control das respostas. Requer Wrangler 4.69.0+. A chave inclui a versão do
worker por default (cross_version_cache off), então um deploy novo começa com cache vazio —
alinhado com a rotação do BUILD_ID.
A configuração usa o padrão gateway + inner:
[exports.default] # GATEWAY — roda TODA request (middleware, geo, de-auth, forward)
[exports.default.cache]
enabled = false # senão a lógica de gateway seria pulada em HITs
[exports.Ssr] # INNER — o SSR propriamente dito
[exports.Ssr.cache]
enabled = true
Interações documentadas no próprio wrangler.toml:
- Num HIT native, o worker não roda.
s-maxage(que o middleware emite) desliga ostale-while-revalidatenative (RFC 9111 §4.2.4) — os dois mecanismos de SWR não se sobrepõem bem. Esta é a incompatibilidade central.- Respostas com
Set-Cookie/no-store/private(auth, tracking) → BYPASS native, o worker roda (e captura tracking) — isso está OK. .data(text/x-turbo-stream) e HTML anônimo público (public, max-age) são cacheáveis nativamente.- Desligar =
enabled = false; não purga o que já foi cacheado.
Camada 1 — SSR Response Cache (Worker Middleware)
Cacheia o HTML de SSR e as respostas .data do React Router no edge. Implementação em
workers/middleware.ts.
Roteamento
| Condição | Comportamento |
|---|---|
| Método ≠ GET | BYPASS |
Path casa NEVER_CACHE_PATTERNS (/api/wallet/, /api/auth/, /api/user/, /api/payments/, /api/kyc/, /api/rewards/, /api/favorites, /api/income-report/, /api/dev/, /mkt) | BYPASS incondicional |
Path é asset (/assets/*) | Asset cache (1y immutable + fallback R2 ASSETS_ARCHIVE) |
Path casa AUTHENTICATED_CACHEABLE_PATTERNS e há sessão | SSR cache autenticado (chave por hash do token) |
Path casa SSR_CACHEABLE_PATTERNS | SSR cache público |
| Demais | Sem cache (default é FALSE — rota não listada nunca é cacheada) |
AUTHENTICATED_CACHEABLE_PATTERNS cobre a área do usuário e as páginas pessoais em todas as
variações de path por marca (/user, /usuario, /jugador, /player, /favorites, /favoritos,
/recents, /jogos-recentes, /recently-played, /jugados-recientemente).
:::caution Ao renomear um path por brand override
SSR_CACHEABLE_PATTERNS e AUTHENTICATED_CACHEABLE_PATTERNS são whitelists de prefixo literal.
Uma marca que troca /games por /cassino precisa do prefixo novo na lista, senão a rota
simplesmente deixa de ser cacheada (falha silenciosa de performance, não de correção).
:::
Chave de cache
a-<generation>-b-_<g|u>_<m|d>_<brand>_<buildId> // público/anônimo
a-<generation>-auth-<hash(jwt)>_<brand>_<buildId> // por sessão
A variação pública é auth × device = 4 buckets. User-Agent e geo não entram na chave
(causavam explosão de namespaces e divergência de staleness entre devices); o segmento auth (g/u)
é invariante de segurança (isolamento guest/user — postmortem de vazamento de saldo,
2026-04-29) e nunca pode ser removido.
Namespaces do CF Cache API: ssr:${BUILD_ID} (público) e ssr-auth:${BUILD_ID} (por sessão) —
caches.open() separados, nunca se misturam. Chave de KV: ssr:${buildId}:${md5(method:url)} e
ssr-auth:${buildId}:${md5(method:url)}.
TTLs
Anônimo público (DEFAULT_TTLS, tunável por env — ver Environment Variables):
| Janela | Default | Env |
|---|---|---|
HTML s-maxage | 60s | SSR_CACHE_HTML_TTL |
HTML stale-while-revalidate | 120s (= 2× o s-maxage) | derivado |
.data s-maxage | 60s | SSR_CACHE_DATA_TTL |
.data stale-while-revalidate | 120s (= 2×) | derivado |
max-age de browser do HTML | = s-maxage do HTML | SSR_CACHE_HTML_BROWSER_TTL |
max-age de browser do .data | 0 (omitido) | SSR_CACHE_DATA_BROWSER_TTL |
| Fallback em KV | 180s | SSR_CACHE_KV_TTL |
Autenticado por sessão (AUTH_DEFAULT_TTLS):
| Janela | Default | Env |
|---|---|---|
HTML s-maxage | 60s | AUTH_SSR_CACHE_HTML_TTL |
.data s-maxage | 30s | AUTH_SSR_CACHE_DATA_TTL |
| Fallback em KV | 600s | constante (AUTH_KV_SSR_TTL) |
O Cache-Control do caminho autenticado é private, … — nunca public.
:::warning O .data está alinhado ao documento DE PROPÓSITO
Antes o .data tinha janela própria (30s/30s). Com relógios diferentes, o primeiro paint (HTML) e a
volta por navegação SPA (.data) expiravam em momentos distintos e serviam fotos de idades
diferentes do MESMO conteúdo — ranking e valores de "Pagou muito" trocavam ao navegar e voltar.
Mesma janela = mesma cadência de expiração. Não desalinhe os dois de novo sem entender esse bug.
:::
:::warning A Cache API IGNORA stale-while-revalidate
O frescor real no edge é só o s-maxage. Passado ele, cache.match dá MISS seco (com
SSR_SWR_ENABLED desligado) e o KV é a única janela stale de fato — é por isso que o valor do KV
importa tanto e por que ele foi reduzido para 180s: um hit de KV repopula o Cache API sem
re-renderizar, então um KV longo transforma um PoP frio numa segunda fonte de verdade com relógio
próprio.
:::
Header X-Cache
Valores reais emitidos pelo middleware:
| Valor | Significado |
|---|---|
HIT | Hit no CF Cache API (caminho público) |
HIT-STALE | Hit servido stale + uma revalidação em background (só com SSR_SWR_ENABLED=true) |
HIT-KV | Miss no Cache API, hit no snapshot de KV (repopula o Cache API) |
HIT-304 | Curto-circuito condicional: If-None-Match bateu com o ETag → 304 sem body, sem ler cache nem renderizar |
HIT-AUTH | Hit no cache por sessão |
HIT-AUTH-KV | Hit no KV do cache por sessão |
MISS | Renderizou (caminho público) |
MISS-AUTH | Renderizou (caminho por sessão) |
BYPASS-AUTH-PUBLIC | Logado em rota pública cacheável — não usa o cache compartilhado |
BYPASS-AUTH-NOSESSION | Rota autenticada-cacheável sem sessão resolvível |
BYPASS-NEVER-CACHE | Path em NEVER_CACHE_PATTERNS |
BYPASS-STALE-BUILD | Gate de build-currency reprovou (worker órfão) — nem lê nem grava |
WARM-STORED / WARM-SKIP | Só no cache warmer: gravou / renderizou mas não gravou |
Gate de build-currency
BUILD_TS + isBuildCurrent() resolvem um problema específico: SHA não tem ordenação, então um
worker que ficou preso como versão órfã (deploy split/gradual) não tem como saber que é velho. O
timestamp de deploy é o critério monotônico "último deploy ganha". Quando o gate reprova, o worker
para de ler e de gravar o SSR cache (X-Cache: BYPASS-STALE-BUILD) e o warmer vira no-op — o
tráfego é servido por SSR fresco. BUILD_TS ausente (dev, pipeline antigo) = gate falha aberto.
Kill-switches que mudam a semântica
Ambos sobem inertes (default OFF) e valem a pena conhecer porque alteram materialmente o comportamento quando ligados.
| Env | Quando "true" |
|---|---|
SSR_SWR_ENABLED | Emula stale-while-revalidate no Cache API (que o ignora nativamente): a entrada é gravada com s-maxage longo + header interno X-Fresh-Until. Passado o frescor lógico, o hit serve o body stale na hora (X-Cache: HIT-STALE) e dispara uma revalidação em background, deduplicada por isolate. O Cache-Control devolvido ao browser e o que vai pro KV não mudam. Só no caminho anônimo público |
SHARED_PUBLIC_DOC_FOR_AUTH | GETs de usuário logado em rotas públicas cacheáveis têm os cookies de auth removidos no entry (deAuthForSharedPublicDoc), então o logado recebe o MESMO documento cacheado do anônimo (TTFB de HIT + SSR completo) e o auth é reidratado 100% no client via /api/auth/profile. A extração de geo roda antes do strip |
Smartico Proxy
/proxy/smartico.js:
- Origem:
https://libs.s.cactusgaming.net/scactus.js - Cache:
s-maxage=7200, stale-while-revalidate=86400 - CORS:
Access-Control-Allow-Origin: *
Em dev, vite-plugins/dev-proxy-mirror.ts espelha a rota no Vite. Marcas devem apontar
gamificationConfig.smartico.libraryUrl para /proxy/smartico.js.
Cache warmer (Cron Trigger)
workers/cache-warmer.ts, disparado pelo [triggers] crons = ["*/2 * * * *"]. Roda in-process
(sem hop externo), então não passa por CF Access nem pelo geo-block: re-renderiza o SSR anônimo dos
paths quentes em modo force-fresh e re-grava o snapshot KV global. O read path do middleware
repopula o Cache API de cada PoP frio a partir do KV, eliminando o MISS completo (SSR + BFF).
A cadência de 2 min é deliberada: mantém margem sob o KV_SSR_TTL de 180s.
Config: CACHE_WARMER_ENABLED, CACHE_WARMER_PATHS, CACHE_WARMER_ORIGIN (ver
Environment Variables para os defaults e a armadilha de host em stage). Cada path é
aquecido em duas variantes de device, porque a chave de cache inclui essa dimensão, e com
User-Agent de bot — o que evita o Set-Cookie de remarketing que reprovaria o guard de gravação
(por contrato de bot.server.ts, o UA de bot não altera a página renderizada).
Paths mal-configurados (fora do whitelist de SSR cacheável, ou em NEVER_CACHE_PATTERNS) são
descartados com aviso antes de gastar SSR+BFF.
Camada 2 — platform-cache engine
Cache lógico de domínio (resources como brandConfig, homeRows, gameStats). Detalhes completos
no SDK: @cactus-agents/platform-cache.
Wire-up no brand worker
app/services/platform-cache.server.ts expõe createPlatformCacheEngine(env):
import { createPlatformCacheEngine } from "~/services/platform-cache.server";
const engine = createPlatformCacheEngine(context?.cloudflare?.env);
O que ele faz internamente:
parseCachePolicy(env.PLATFORM_CACHE_POLICY_JSON)— ausente/inválido = engine em BYPASS (chama os fetchers direto; zero configuração para rodar local).CacheApiStoresingleton por generation, guardado em módulo — preserva o tier de memória entre requests do mesmo isolate. O namespace do CF Cache API éplatform-cache-<CACHE_GENERATION>.KvSnapshotStore(env.PLATFORM_CACHE_KV)quando o binding existe, senãoNoopSnapshotStore.domain = env.CACHE_NAMESPACE ?? env.ORIGIN_DOMAIN— prefixo de toda chave.
TTLs canônicos (PROD)
Definidos em front-ops/config/cache/defaults.yml — essa é a fonte de verdade; a tabela abaixo
é um resumo e pode ficar atrás dela.
| Resource | TTL | SWR | KV snapshot | Tags |
|---|---|---|---|---|
brandConfig | 60s | 300s | Sim | brand, config |
homeRows | 3600s | 600s | Sim | catalog, rows, games:list, home |
casinoRows | 3600s | 600s | Sim | catalog, rows, games:list, casino |
casinoLiveRows | 3600s | 600s | Sim | catalog, rows, games:list, casino, live |
gamesBase | 3600s | 300s | Sim | catalog, games:base |
legalTerms | 3600s | 300s | Não | legal, config |
topWins | 1800s | 120s | Não | catalog, stats, games:wins |
lastWins | 1800s | 120s | Não | catalog, stats, games:wins |
topGames | 3600s | 600s | Sim | catalog, stats, games:high-payers |
gameStats | 1800s | 1800s | Não | catalog, stats, games:stats |
gameDetail | 3600s | 600s | Não | catalog, games:detail |
gameTopWins | 300s | 120s | Não | catalog, stats, games:wins |
allGames | 3600s | 600s | Sim | catalog, games:list, games:all |
gameCategory | 3600s | 600s | Não | catalog, games:category |
gameProvider | 3600s | 600s | Não | catalog, games:provider |
gameListPage | 300s | 300s | Não | catalog, games:list, games:listPage |
content | 600s | 1800s | Sim | content |
contentList | 300s | 600s | Não | content |
sitemap | 3600s | 600s | Sim | seo, sitemap |
Há ainda o resource userFavorites, usado por FavoritesCacheService com chave por userId
(engine.fetchWithKey) — ver Favorite Games.
Marcas podem sobrescrever por env em config/brands/<brand>/environments/<env>/cache.yml
(substituição completa por nível).
Pipeline de fetch
- Primary store (memory → CF Cache API)
- Se cache-only, para aqui
- KV snapshot (se
fallbackEnabled: true) - Origin fetch
- Em erro de origin: retry no primary com grace de
staleIfError - Em erro de origin sem cache: KV snapshot como último recurso
Single-flight coalescing por isolate evita thundering herd. O SWR grava o resultado novo em
background via ctx.waitUntil.
Camada 3 — service-api proxy
front-service-api é um Worker separado com Smart Placement (co-locado com o BFF), chamado via
Service Binding API_SERVICE. Cacheia respostas HTTP do BFF em CF Cache API + KV, com TTLs por
prefixo de path (vocabulário diferente do platform-cache, que é por nome de resource).
Regras em front-ops/config/cache/service-api-defaults.yml, injetadas como
SERVICE_API_CACHE_POLICY_JSON no deploy do worker. Não se chama direto — apenas via o binding.
Desde a remoção da camada api:, ele também assume o cache de /v2/cactus-sportbook/search: o
ApiClient.proxyRaw passou a ser binding-aware no core, então /api/cactus-sportbook/* — que era
o único path que fazia fetch direto — também atravessa o proxy.
Camada 4 — caches do browser
O base não instancia ApiClient no browser: todo o tráfego client passa por rotas same-origin
/api/* com fetch. Então o LocalStorageCache do api-client não é populado aqui, e a chamada que
o limpa é defensiva.
O que de fato cacheia no cliente:
| Mecanismo | Detalhe |
|---|---|
| Cache HTTP do browser | max-age emitido pelo middleware (SSR_CACHE_HTML_BROWSER_TTL / SSR_CACHE_DATA_BROWSER_TTL). Não é build-gated: a partir do deploy, o cache do próprio cliente segue servindo até o max-age expirar — o gate de frescor do SW + ETag/304 é que limitam a staleness real |
| Service Worker | public/sw.js, CACHE_NAME = app-apcsrasj-v6-<BUILD_ID>. O activate apaga todos os caches cujo nome difere do atual |
| localStorage / sessionStorage da aplicação | Caches próprios de feature (ex: favoritos em brand:favorites:<userId>, snapshot do header do usuário, snapshots de sessão de rota) |
/api/clear-cache | Endpoint de version-check: o client manda X-Version-Id; se não bate com o BUILD_ID atual, a resposta vem com Clear-Site-Data: "cache" forçando o browser a dropar os caches HTTP |
purgeAllClientCaches() (app/utils/cache-purge.client.ts) é o ponto único chamado no logout e no
logout forçado por 401 — limpa o snapshot do header, o cookie gm_id, o cache in-memory do
api-client e o cache de feature flags legadas. Postmortem de referência: vazamento de saldo,
2026-04-29.
Purge
Endpoints
| Endpoint | Método | Auth | Função |
|---|---|---|---|
/api/cache/purge | POST (aceita também query string) | X-Cache-Secret | Purge do platform-cache + cascade no service-api. Aceita segments, tags, glob, only |
/api/cache/inspect | GET | X-Cache-Secret | Estado live do platform-cache (TTL restante por entry, filtro por ?tag=) |
/api/dev/cache-policy | GET | — | Lista a policy carregada. Só existe em build de dev |
/api/dev/cache-clear | POST | — | purgeAll(). Só existe em build de dev |
As rotas api/dev/* não são registradas em builds de produção e o Vite tree-shaka os arquivos do
bundle do worker — ver Routing.
Body do /api/cache/purge
// Por tags (preferido)
{ "tags": ["catalog", "brand"] }
// Por glob de tag
{ "glob": "games:*" }
// Por segments (legado, ainda funciona)
{ "segments": ["homeRows", "casinoRows"] }
// Scope (gate de qual camada atua): "platform-cache" | "service-api" | "ssr" | "all"
{ "tags": ["brand"], "only": "platform-cache" }
{ "tags": ["brand"], "only": "service-api" }
// (omitir "only" = comportamento default)
// Sem nada = purgeAll
{}
Workflows GH Actions (front-ops)
| Workflow | Trigger | Função |
|---|---|---|
manual-cache-purge.yml | workflow_dispatch | Purge sob demanda (environment, tags/glob/segments, scope, dry_run) |
cache-status.yml | workflow_dispatch | Inspeciona estado live (TTLs restantes, tags por resource) |
bump-cache-generation.yml | workflow_dispatch | Incrementa o current: do generation.yml e redeploya o escopo escolhido |
purge-all.yml / purge-monitor.yml | workflow_dispatch | Purge amplo e monitoração |
deploy.yml | push / dispatch | Injeta as vars de cache e faz purge pós-deploy (gateável por env via skip_post_deploy_purge); também configura o cache warmer por ambiente |
Arquivos
| Arquivo | Descrição |
|---|---|
workers/middleware.ts | SSR cache (público + por sessão), asset cache, Smartico proxy, gate de build-currency |
workers/cache-warmer.ts | Lógica pura + orquestração do warm (o handler scheduled a invoca) |
app/services/platform-cache.server.ts | Singleton do CacheApiStore por generation + createPlatformCacheEngine |
app/services/brand.server.ts | engine.fetch("brandConfig", …) |
app/services/games.cache.server.ts | GamesCacheService — todos os resources de games |
app/services/favorites.cache.server.ts | FavoritesCacheService — resource userFavorites, chave por userId |
app/services/api.server.ts | createClient com serverCache.delegateCaching: true |
app/routes/api/cache/purge.ts | Endpoint de purge (tags/glob/segments/only) |
app/routes/api/cache/inspect.ts | Endpoint de introspecção live |
app/routes/api/clear-cache.ts | Version-check + Clear-Site-Data para o browser |
app/utils/cache-purge.client.ts | purgeAllClientCaches() — limpeza no logout |
public/sw.js | Cache do Service Worker (app-apcsrasj-v6-<BUILD_ID>) |