Personal study reference, not customer-facing. A practical playbook for finding where Workers AI / AI Gateway tokens go and cutting them without cutting quality — updated and re-verified against current docs (original was dated July 2026; several facts, a deprecated model, and a major new feature — Auto Router — have changed since). Every fact below is sourced from live Cloudflare docs (full list at the bottom).
Six root causes, covered in the order they usually drive real dollar problems. For each: how to spot it in AI Gateway, how to fix it, and code to drop in.
Why AI Gateway matters here. You can't fix what you can't measure. AI Gateway is the observability layer that turns "our AI bill is high" into "request #47 spent 12,000 tokens because the system prompt was 8,000 tokens and we sent it 47 times today."
| # | Root cause | Typical token waste | Fix effort |
|---|---|---|---|
| 1 | No response cache | 20–60% of tokens | 10 minutes |
| 2 | Huge system prompt resent every request | 30–70% of input tokens (agent workloads) | 1 hour |
| 3 | No max_tokens ceiling | 10–40% of output tokens | 15 minutes |
| 4 | Wrong model for the job | Cost multiplier of 10–50× | 1 day (eval work) |
| 5 | Chatty conversation history | Grows linearly with turns, unbounded | 2 hours |
| 6 | Retries & duplicate requests | 5–20% of all tokens | 10 minutes |
The single fastest cost win. If any part of your app asks the model the same question more than once, every duplicate is 100% wasted spend.
AI Gateway → Analytics → Cached Requests percentage. Turn caching on for 24 hours and see what percentage of traffic is exactly-duplicate — that's your savings ceiling.
Caching is exact-match only. Two prompts differing by a single character are two cache entries. Semantic/similarity caching is still roadmap-only as of today — not shipped.
const resp = await env.AI.run(
"@cf/meta/llama-3.1-8b-instruct-fast",
{ messages: [{ role: "user", content: prompt }] },
{
gateway: {
id: "your-gateway-id",
skipCache: false,
cacheTtl: 3600,
cacheKey: "optional-custom-key", // not in the original playbook
collectLog: true, // not in the original playbook
},
}
);
Header rename to know about: the per-request HTTP headers cf-cache-ttl and cf-skip-cache are now cf-aig-cache-ttl and cf-aig-skip-cache. Old names still work but are deprecated — use the new ones going forward.
Live proof. On cf-demo-app, a repeat identical RAG query: cold path 1,507ms → cached hit 144ms (10× faster), and zero tokens billed on the cached hit.
The silent killer for agentic workloads. An 8,000-token system prompt sent 500 times a day is 4M input tokens before the user says a word.
AI Gateway → Logs → open any request's input body. System message over 500 tokens and identical across requests? That's you. Analytics' tokens-per-request average over 3,000 suggests a large shared prefix.
Workers AI has prefix caching built in for supported models. Cached prefix tokens bill at a discounted rate and skip the prefill compute stage.
const response = await env.AI.run(
"@cf/moonshotai/kimi-k2.6",
{
messages: [
{ role: "system", content: LONG_SYSTEM_PROMPT }, // static, front-loaded
{ role: "user", content: userQuery }, // dynamic, at the end
],
},
{ extraHeaders: { "x-session-affinity": "ses_" + sessionId } }
);
Field-verification correction: the cached-token count is not a top-level usage.cached_tokens field — it's nested at usage.prompt_tokens_details.cached_tokens. Update any code checking the old shape.
Model list correction — kimi-k2.5 is deprecated (removed May 30, 2026). Current prefix-caching-supported models include kimi-k2.6, kimi-k2.7-code, glm-5.2, glm-5.3, glm-5.3-flash, and deepseek-v4-flash/pro. Notably, glm-4.7-flash has no cached-token pricing tier — don't claim it supports prefix-caching discounts.
max_tokens ceilingWithout max_tokens, the model generates until it decides to stop. On expensive models, a runaway response can cost 40× what the actual answer needed.
AI Gateway → Analytics → Output tokens per request. High variance (P50=200, P95=3,500) is the tell.
Set max_tokens to 1.5× the longest useful answer. Chat: 500–1000. Summaries: 300–500. Code: 1500–2500. Unbounded is a bug.
const resp = await env.AI.run(
"@cf/meta/llama-3.3-70b-instruct-fp8-fast",
{ messages: [...], max_tokens: 500, temperature: 0.3 }
);
Bonus tactic: add a length instruction to the system prompt — "Keep responses under 4 sentences unless explicitly asked for more." Costs zero extra tokens once cached.
Most workloads don't need a 200B-parameter model. Prices differ by 10–50× across the catalog.
| Model | Input $/M | Output $/M | Cached $/M | Good for |
|---|---|---|---|---|
llama-3.3-70b-instruct-fp8-fast | $0.293 | $2.253 | — | General reasoning, RAG, mid-complexity |
glm-4.7-flash | $0.060 | $0.400 | — | Fast classification, routing, tool-calling |
gpt-oss-20b | $0.200 | $0.300 | — | Cheap classification/routing alternative |
glm-5.3-flash | $0.150 | $0.500 | $0.030 | New — approaches larger-model coding/agentic quality at a fraction of the cost |
glm-5.2 | $1.400 | $4.400 | $0.260 | Long-context reasoning, complex agents |
glm-5.3 | $1.400 | $4.400 | $0.260 | New, 1M context agentic coding |
kimi-k2.6 | $0.950 | $4.000 | $0.160 | New frontier model, replaces deprecated kimi-k2.5 |
kimi-k2.7-code | $0.950 | $4.000 | $0.190 | Code generation, tool-use agents (now has concrete pricing — previously listed as "varies") |
nemotron-3-120b-a12b | $0.500 | $1.500 | — | New, hybrid Mamba-transformer, built for multi-agent |
One pricing discrepancy flagged for hands-on confirmation: @cf/meta/llama-3.1-8b-instruct-fast is confirmed still active (not deprecated) but does not appear in the current aggregate pricing table — only the -fp8-fast variant does, at $0.045 in / $0.384 out. Verify this specific model's live price in the dashboard before quoting it to a customer.
Deprecated since the original playbook: kimi-k2.5 (May 30, 2026) plus several older base models — llama-3-8b-instruct(-awq), llama-3.1-8b-instruct(-awq), llama-3.1-70b-instruct, llama-2-7b-chat variants, mistral-7b-instruct-v0.1, gemma-7b-it, gemma-3-12b-it, phi-2. The -fast and -lora suffixed variants were explicitly preserved.
// 1. Cheap classifier call
const route = await env.AI.run(
"@cf/zai-org/glm-4.7-flash",
{
messages: [
{ role: "system", content: "Classify this request as one of: SIMPLE, COMPLEX, CODE. Respond with only the word." },
{ role: "user", content: userQuery },
],
max_tokens: 5,
}
);
// 2. Route to the right-sized model
const model = route.response.includes("CODE") ? "@cf/moonshotai/kimi-k2.7-code"
: route.response.includes("COMPLEX") ? "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
: "@cf/zai-org/glm-4.7-flash";
const answer = await env.AI.run(model, { messages: [...] });
New since the original playbook — Auto Router. AI Gateway now ships a built-in cloudflare/auto model string that automatically classifies task difficulty and routes to the cheapest model that can handle it, with session affinity (cf-aig-session-id) to preserve prompt-cache hits across a multi-turn session. This is a purpose-built, zero-code-maintenance replacement for the manual classifier-routing pattern above — worth leading with instead of hand-rolling the pattern for new builds.
Don't skip evals. Build a labelled test set of 50–100 real production requests with known-good answers before swapping models. AI Gateway logs give you the production data to build that set — export requests, replay against the cheaper model, compare.
Naive chat implementations send the entire conversation history every turn. Input tokens grow linearly, cost grows quadratically. Users don't feel it until the bill arrives.
AI Gateway → Logs, sort by input tokens descending. Top requests at 20k+ input tokens with the same "hello" from 40 turns ago still in the list? That's you.
Blindly appending every user + assistant message to a list forever, then sending the whole list every call.
const RECENT_TURNS = 6;
const older = history.slice(0, -RECENT_TURNS);
const recent = history.slice(-RECENT_TURNS);
let summary = "";
if (older.length > 0) {
summary = await summarizeOnce(older); // cheap model, ~300 tokens out
}
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
...(summary ? [{ role: "system", content: "Prior context: " + summary }] : []),
...recent,
{ role: "user", content: userQuery },
];
Compounds well with Cause 2. Keep the system prompt stable and recent turns at the end — prefix caching kicks in for the shared history, saving on both prompt token count and cached-tier pricing for what remains.
Client retries on timeout, frontend double-clicks, queue redelivery — all bill twice. 5–20% of AI spend on undisciplined codebases is duplicates.
AI Gateway → Logs, filter by user/session ID, look for identical prompts within seconds of each other. Or compare request count to unique-user count in Analytics.
Layer 1: response cache (Cause 1) turns the duplicate into a free cache hit. Layer 2: app-level idempotency — dedupe by request hash before calling the gateway.
AI Gateway Settings → Rate Limiting → set a per-token or per-IP cap. Won't fix bad code, but stops a runaway loop from becoming a runaway bill.
AI Gateway Settings → Retry Requests → configure backoff (current defaults: up to 5 retries, 0.1–60s delay, Constant/Linear/Exponential backoff). Then remove the retry loop from client code — one place to reason about retries beats a dozen scattered implementations.
| Setting | Fixes cause | Recommended default |
|---|---|---|
| Collect Logs | All 6 — diagnosis foundation | ON |
| Cache Responses | #1, #6 | ON, TTL 3600s (off by default on new gateways) |
| Rate Limiting | #6 — blast-radius cap | Start at 100 req/min per token, tune |
| Spend Limits | All — hard budget stop | Set to 120% of expected monthly; scope by cf.user_id or custom metadata |
| Retry Requests | #6 — centralize retries | 2 retries, exponential backoff |
| Authenticated Gateway | Stops unauthorized traffic from spending your tokens | ON |
Spend Limits is now GA — it launched as Beta June 2026 but no longer carries a Beta label as of the current Features page, now listed as a standard Cost Optimization feature. Max 20 rules per gateway; default breach behavior is a 429 block, with an optional fallback-to-cheaper-model via Dynamic Routing.
await env.AI.run(
"@cf/mistral/mistral-7b-instruct-v0.1",
{ prompt: "..." },
{ gateway: { id: "gateway_id", metadata: { team: "AI", user_id: 12345 } } }
);
Max 5 metadata entries per request; values must be string/number/boolean, no nested objects. If the gateway sits behind Cloudflare Access, the authenticated user ID is automatically attached as the reserved key cf.user_id — you don't need to pass it yourself, and customer-supplied keys starting with cf. are stripped. Without Access, pick your own key (e.g. user_id) and reference that exact name in Dynamic Route conditions / Spend Limit dimensions.
| Feature | Why it matters for cost |
|---|---|
Auto Router (cloudflare/auto) New | Built-in difficulty-aware routing to the cheapest capable model, with session affinity to preserve prompt-cache hits — see Module 5 |
| User Insights — "Potential Savings" view New | Directly flags which requests could use a cheaper/faster model without quality loss — free, zero setup, probably the single most relevant new cost feature |
| Workers AI + AI Gateway billing unification New | Workers AI can now bill from prepaid AI Gateway credits; frontier models get 50 RPM (vs. 20 RPM standard) when paid via credits — changes capacity planning math |
| Custom Costs — cache-read/write rates New | Needed if you've negotiated cache pricing directly with a provider |
| Web Search API Beta | Lets agents ground responses instead of burning tokens on hallucination-correction loops |
Hands-on notes from Fallback and Dynamic Routing. Live demo: cf-demo-app.dustinburke23nc.workers.dev/ai-gateway.
What these are. Fallback Routing defines a chain of models — if the primary fails or rate-limits, the gateway automatically tries the next one. Dynamic Routing is the visual pipeline builder composing Rate Limit, Budget Limit, Model, and Fallback nodes into a route. Both map directly onto Cause 6's "seatbelt" pattern. Neither carries a Beta label on the current Features page anymore — treat as likely-GA, though no explicit "now GA" changelog post was found.
Basic Fallback. Solid — if the primary model drops offline, the gateway routes to the fallback with no client-side change.
Per-user rate limiting on metadata. Worked once caching was off. In the demo, a heavy user gets blocked after their rate ceiling while a separate user is unaffected — the gateway blocks blast-radius requests before they reach any provider, so blocked calls cost zero tokens.
Important: these three issues were observed in July 2026. They are implementation details of a beta visual editor, not documented API contracts — current docs can't confirm whether they're fixed. The surrounding ecosystem (Spend Limits going GA, Auto Router shipping, standardized Dynamic Routing docs updated as recently as Oct 2, 2026) has matured substantially since. Retest directly in the dashboard before citing these as current in a customer conversation.
| Customer question | Honest answer |
|---|---|
| "Can we set a hard dollar cap?" | Yes — use Spend Limits (now GA) as the real hard cap on the platform. Use Dynamic Routing's Rate Limit node for request-count guardrails specifically. |
| "Can we downgrade to a cheaper model when a rate limit hits?" | Verify live — this had a known UI bug as of mid-2026. If still broken, position it as "we block the abuser at the gateway and save the LLM cost" instead, which is arguably the better cost story anyway. |
| "Does caching still work alongside routing?" | Yes, and that's desirable — cache hits cost nothing. Confirm live whether cached requests still bypass rate-limit counters before promising exact behavior. |
| "Is this production-ready?" | Fallback Routing: yes. Dynamic Routing: treat as mature but verify current UI behavior before a live customer demo, given known July-era rough edges. |
If nothing else after this call, do these six things in order. ~30 minutes total. Expected cost reduction on a typical noisy workload: 30–50%.
gateway: { id: "..." } to every env.AI.run() call.max_tokens on every call — 10 min. Search the codebase for env.AI.run, add a ceiling to each.x-session-affinity to agent workloads — 10 min. Stable long system prompts become 30–70% cheaper.After 24 hours of production traffic, check Analytics. Cached % should be non-zero, average input tokens lower than yesterday.
glm-4.7-flash notably does not have a cached-token tier.Click a question to reveal the answer. No peeking before you commit to one.
Turning on response caching (Cause 1). Its ceiling: caching is exact-match only — two prompts differing by a single character are two separate cache entries. Semantic caching is still roadmap-only, not shipped.
False — caching defaults to OFF on a new gateway. Don't assume it's already saving money without checking Settings.
It's nested at usage.prompt_tokens_details.cached_tokens, not a top-level usage.cached_tokens field. Checking the wrong path will always read as zero/undefined even when caching is working.
kimi-k2.5, deprecated May 30, 2026 — replaced by kimi-k2.6 and kimi-k2.7-code.
No — it has no published cached-token pricing tier, unlike glm-5.2/glm-5.3/kimi-k2.6/kimi-k2.7-code. Don't claim prefix-caching savings for it.
A built-in cloudflare/auto model string that automatically classifies task difficulty and routes to the cheapest capable model, with session affinity to preserve prompt-cache hits. It's a feature that shipped after the original July 2026 playbook was written — it replaces the need to hand-roll a classifier-based model-tiering pattern.
No — it launched as Beta in June 2026 but no longer carries a Beta label on the current Features page; it's treated as GA.
The authenticated user ID is automatically attached as the reserved key cf.user_id — you don't pass it yourself. The gotcha: keys starting with cf. are reserved, so a customer-supplied metadata field named cf.user_id would be silently stripped.
Send the x-session-affinity header with a stable session/agent identifier via extraHeaders in the env.AI.run() call options — not a dashboard toggle.
Any two of: cacheKey (custom cache key), collectLog (boolean), metadata (object, max 5 entries).
cf-aig-skip-cache (and cf-cache-ttl → cf-aig-cache-ttl). Old names still function but are deprecated.
Spend Limits is the real hard budget cap. Dynamic Routing's Rate Limit node is for request-count guardrails, not dollar caps — don't conflate the two.
(1) Budget Limit node only accepted whole-dollar integers, (2) a Rate Limit node's fallback-model branch kept reverting on save, (3) cached responses bypassed the Rate Limit counter entirely. Correct current framing: these can't be confirmed fixed or still-broken from docs alone — the beta UI has matured significantly since, so retest live before citing them as current in a customer conversation.
If the system prompt stays stable and only recent turns are appended at the end, prefix caching can still match the shared, unchanged prefix — saving both on raw prompt token count (fewer history tokens) and on cached-tier pricing for what remains.
They're multiplicative on the remaining cost, not additive on the percentage. A 30% reduction plus a 20% reduction compounds to about 44% combined (0.7 × 0.8 = 0.56, i.e. 44% saved), not 50%.
The User Insights "Potential Savings" view — arguably the most directly relevant new cost-optimization feature to ship since the original playbook.
Frontier models get 50 RPM when paid via AI Gateway credits, versus the standard 20 RPM — a direct result of the Workers AI + AI Gateway billing unification. Relevant for capacity planning on high-throughput agentic workloads.