Pular para o conteúdo principal

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

  1. api-client server-side está desabilitado no brand worker via serverCache.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).
  2. 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 binding API_SERVICE e o front-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:

FonteValorPapel
front-ops/config/cache/generation.yml (current:)rotacionado com frequênciaFonte 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-v3Fallback in-source, usado só quando nenhuma env var é injetada
workers/middleware.ts (resolveGeneration) e app/services/platform-cache.server.tsapcsrasj-v5Fallback 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 o stale-while-revalidate native (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çãoComportamento
Método ≠ GETBYPASS
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ãoSSR cache autenticado (chave por hash do token)
Path casa SSR_CACHEABLE_PATTERNSSSR cache público
DemaisSem 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):

JanelaDefaultEnv
HTML s-maxage60sSSR_CACHE_HTML_TTL
HTML stale-while-revalidate120s (= 2× o s-maxage)derivado
.data s-maxage60sSSR_CACHE_DATA_TTL
.data stale-while-revalidate120s (= 2×)derivado
max-age de browser do HTML= s-maxage do HTMLSSR_CACHE_HTML_BROWSER_TTL
max-age de browser do .data0 (omitido)SSR_CACHE_DATA_BROWSER_TTL
Fallback em KV180sSSR_CACHE_KV_TTL

Autenticado por sessão (AUTH_DEFAULT_TTLS):

JanelaDefaultEnv
HTML s-maxage60sAUTH_SSR_CACHE_HTML_TTL
.data s-maxage30sAUTH_SSR_CACHE_DATA_TTL
Fallback em KV600sconstante (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.matchMISS 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:

ValorSignificado
HITHit no CF Cache API (caminho público)
HIT-STALEHit servido stale + uma revalidação em background (só com SSR_SWR_ENABLED=true)
HIT-KVMiss no Cache API, hit no snapshot de KV (repopula o Cache API)
HIT-304Curto-circuito condicional: If-None-Match bateu com o ETag → 304 sem body, sem ler cache nem renderizar
HIT-AUTHHit no cache por sessão
HIT-AUTH-KVHit no KV do cache por sessão
MISSRenderizou (caminho público)
MISS-AUTHRenderizou (caminho por sessão)
BYPASS-AUTH-PUBLICLogado em rota pública cacheável — não usa o cache compartilhado
BYPASS-AUTH-NOSESSIONRota autenticada-cacheável sem sessão resolvível
BYPASS-NEVER-CACHEPath em NEVER_CACHE_PATTERNS
BYPASS-STALE-BUILDGate de build-currency reprovou (worker órfão) — nem lê nem grava
WARM-STORED / WARM-SKIPSó 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.

EnvQuando "true"
SSR_SWR_ENABLEDEmula 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_AUTHGETs 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).
  • CacheApiStore singleton 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ão NoopSnapshotStore.
  • domain = env.CACHE_NAMESPACE ?? env.ORIGIN_DOMAIN — prefixo de toda chave.

TTLs canônicos (PROD)

Definidos em front-ops/config/cache/defaults.ymlessa é a fonte de verdade; a tabela abaixo é um resumo e pode ficar atrás dela.

ResourceTTLSWRKV snapshotTags
brandConfig60s300sSimbrand, config
homeRows3600s600sSimcatalog, rows, games:list, home
casinoRows3600s600sSimcatalog, rows, games:list, casino
casinoLiveRows3600s600sSimcatalog, rows, games:list, casino, live
gamesBase3600s300sSimcatalog, games:base
legalTerms3600s300sNãolegal, config
topWins1800s120sNãocatalog, stats, games:wins
lastWins1800s120sNãocatalog, stats, games:wins
topGames3600s600sSimcatalog, stats, games:high-payers
gameStats1800s1800sNãocatalog, stats, games:stats
gameDetail3600s600sNãocatalog, games:detail
gameTopWins300s120sNãocatalog, stats, games:wins
allGames3600s600sSimcatalog, games:list, games:all
gameCategory3600s600sNãocatalog, games:category
gameProvider3600s600sNãocatalog, games:provider
gameListPage300s300sNãocatalog, games:list, games:listPage
content600s1800sSimcontent
contentList300s600sNãocontent
sitemap3600s600sSimseo, 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

  1. Primary store (memory → CF Cache API)
  2. Se cache-only, para aqui
  3. KV snapshot (se fallbackEnabled: true)
  4. Origin fetch
  5. Em erro de origin: retry no primary com grace de staleIfError
  6. 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:

MecanismoDetalhe
Cache HTTP do browsermax-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 Workerpublic/sw.js, CACHE_NAME = app-apcsrasj-v6-<BUILD_ID>. O activate apaga todos os caches cujo nome difere do atual
localStorage / sessionStorage da aplicaçãoCaches próprios de feature (ex: favoritos em brand:favorites:<userId>, snapshot do header do usuário, snapshots de sessão de rota)
/api/clear-cacheEndpoint 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

EndpointMétodoAuthFunção
/api/cache/purgePOST (aceita também query string)X-Cache-SecretPurge do platform-cache + cascade no service-api. Aceita segments, tags, glob, only
/api/cache/inspectGETX-Cache-SecretEstado live do platform-cache (TTL restante por entry, filtro por ?tag=)
/api/dev/cache-policyGETLista a policy carregada. Só existe em build de dev
/api/dev/cache-clearPOSTpurgeAll(). 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)

WorkflowTriggerFunção
manual-cache-purge.ymlworkflow_dispatchPurge sob demanda (environment, tags/glob/segments, scope, dry_run)
cache-status.ymlworkflow_dispatchInspeciona estado live (TTLs restantes, tags por resource)
bump-cache-generation.ymlworkflow_dispatchIncrementa o current: do generation.yml e redeploya o escopo escolhido
purge-all.yml / purge-monitor.ymlworkflow_dispatchPurge amplo e monitoração
deploy.ymlpush / dispatchInjeta 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

ArquivoDescrição
workers/middleware.tsSSR cache (público + por sessão), asset cache, Smartico proxy, gate de build-currency
workers/cache-warmer.tsLógica pura + orquestração do warm (o handler scheduled a invoca)
app/services/platform-cache.server.tsSingleton do CacheApiStore por generation + createPlatformCacheEngine
app/services/brand.server.tsengine.fetch("brandConfig", …)
app/services/games.cache.server.tsGamesCacheService — todos os resources de games
app/services/favorites.cache.server.tsFavoritesCacheService — resource userFavorites, chave por userId
app/services/api.server.tscreateClient com serverCache.delegateCaching: true
app/routes/api/cache/purge.tsEndpoint de purge (tags/glob/segments/only)
app/routes/api/cache/inspect.tsEndpoint de introspecção live
app/routes/api/clear-cache.tsVersion-check + Clear-Site-Data para o browser
app/utils/cache-purge.client.tspurgeAllClientCaches() — limpeza no logout
public/sw.jsCache do Service Worker (app-apcsrasj-v6-<BUILD_ID>)