Cloudflare Workers AI Free Tier: What 10,000 Neurons Buy
The Cloudflare Workers AI free tier gives you 10,000 neurons a day, and what that buys depends far more on the model and the prompt size than the per-day estimates in search results suggest. I sent 30 calls on 2026-10-12 and read the neurons each one was charged from the cf-ai-neurons response header. A short answer from a small model cost 0.70 to 3.44 neurons, so 2,900 to 14,000 calls a day. The same models with a 1,800-token prompt cost 5 to 30 neurons. llama-3.3-70b cost 9.78 for a short answer and 78.29 with the long prompt, about 127 a day. Embedding a 500-token page with bge-base-en-v1.5 cost 2.99. One FLUX.1 schnell image at the default 4 steps cost 172.80, so 57 images use the whole day.
What 10,000 Neurons Bought, per Call
These are the headline numbers. Every figure is the metered value Cloudflare returned for one call on 2026-10-12 (UTC), with max_tokens at 256 unless I say otherwise:
- Smallest text model,
@cf/: 0.70 neurons for an 80-word answer. About 14,000 a day.ibm-granite/ granite-4.0-h-micro - A 1B Llama with a 500-token document in the prompt: 4.11 neurons. About 2,400 a day.
- An 8B Llama (
llama-3.1-8b-instruct-fp8) reviewing an 1,800-token script: 30.42 neurons. About 328 a day. - A 70B Llama on the same script: 78.29 neurons. About 127 a day.
- A reasoning model,
gpt-oss-20b: 8.55 to 40.91 neurons for answers that came back empty atmax_tokens256. With 1,024 it answered, for 11.93 to 44.45. - Embeddings: 0.10 for one sentence, 2.99 for a 500-token page.
- Images: 172.80 for one 1024 by 1024 FLUX.1 schnell image at 4 steps, 57.60 at 1 step.
Rates and the catalog change: Cloudflare edited the Workers AI pricing page on 2026-10-09. Rerun the script below before you plan around these numbers.
Workers AI Reports the Neurons on Every Response
The pricing page only says you can monitor usage in the dashboard. I sent one tiny call to POST /accounts/<ACCOUNT_ID>/ and listed the response header names:
api-version cache-control cf-ai-neurons cf-ai-req-id cf-auditlog-id cf-ray connection
content-encoding content-type date server set-cookie transfer-encoding
cf-ai-neurons carries the neurons for that request, rounded to two decimals. It was present on all 30 successful calls: text, embeddings and images. All five text models I tested and FLUX also return usage.neurons in the JSON body at full precision; bge-base-en-v1.5 returned token counts but no neurons field. I found neither field in the Workers AI docs on 2026-10-12, so they could change without notice.
Cloudflare's GraphQL API also has an aiInferenceAdaptiveGroups dataset with a totalNeurons sum. My token had only Workers AI Read permission, and the query failed (trimmed):
{"message":"not authorized for that account","extensions":{"code":"authz"}}
So the numbers here are metered per request by the header, and I compared each with the neurons computed from the published rates. I did not cross-check them against the dashboard.
The Test Setup
A Node 25 script on Windows 11 called the REST API. The account is on Workers Paid, so going past 10,000 neurons would have billed; the script refuses any call whose worst case would push my ledger past 2,400. The whole test, 31 requests, used about 640 neurons. Three prompt sizes, the same for every text model:
- S: "What does an HTTP ETag header do? Answer in at most 80 words."
- M: 400 words of notes on an upload service plus a question about retries. About 500 tokens in.
- L: an earlier version of the script itself plus "list the three biggest risks". About 1,800 tokens in.
I copied the model ids from the catalog, which listed 70 models on 2026-10-12. Each configuration ran once, so output lengths and latencies are single samples: the same short prompt on granite gave 62 output tokens in one run and 74 in another.
The Measured Table
Tokens in and out come from usage in the response, neurons from the cf-ai-neurons header, and "per 10k" is 10,000 divided by the unrounded neurons. Latency is wall time from the machine that ran the script, one sample each.
model size in out neurons per 10k latency
granite-4.0-h-micro S 47 62 0.70 14,239 3,184 ms
granite-4.0-h-micro M 526 95 1.78 5,630 3,349 ms
granite-4.0-h-micro L 1,802 256 5.38 1,859 7,296 ms
llama-3.2-1b-instruct S 28 75 1.44 6,955 491 ms
llama-3.2-1b-instruct M 507 157 4.11 2,432 1,074 ms
llama-3.2-1b-instruct L 1,783 256 9.05 1,104 1,763 ms
llama-3.1-8b-instruct-fp8 S 28 117 3.44 2,904 5,109 ms
llama-3.1-8b-instruct-fp8 M 507 147 10.83 923 6,708 ms
llama-3.1-8b-instruct-fp8 L 1,783 224 30.42 328 10,738 ms
llama-3.3-70b-instruct-fp8-fast S 52 41 9.78 1,022 1,023 ms
llama-3.3-70b-instruct-fp8-fast M 531 77 29.93 334 1,415 ms
llama-3.3-70b-instruct-fp8-fast L 1,807 147 78.29 127 3,083 ms
gpt-oss-20b, max_tokens 1024 S 86 380 11.93 838 2,697 ms
gpt-oss-20b, max_tokens 1024 M 563 672 28.56 350 4,614 ms
gpt-oss-20b, max_tokens 1024 L 1,866 386 44.45 224 2,821 ms
bge-base-en-v1.5, 1 sentence 17 0 0.10 97,087 84 ms
bge-base-en-v1.5, 1 page 493 0 2.99 3,347 121 ms
bge-base-en-v1.5, 19 sentences 912 0 5.52 1,809 158 ms
flux-1-schnell, 1 step - - 57.60 173 732 ms
flux-1-schnell, 4 steps - - 172.80 57 1,384 ms
The full ids carry the @cf/ prefix and vendor, for example @cf/. For every text and embedding call, the header matched tokens times the published rate to the second decimal. For images it did not.
The Prompt Costs More Than the Rate Card Suggests
Output rates are several times input rates for most models, 7.7 times for the 70B, so it is easy to look only at output. Yet in my long-prompt runs, input was more than half the bill on four of the five text models. The 70B call read 1,807 tokens and wrote 147: 48.19 of its 78.29 neurons were input. A chatbot that resends the whole conversation spends the free tier on input.
The model choice matters as much. llama-3.2-1b-instruct answered the retry question for 4.11 neurons against 10.83 for the 8B. My test does not settle quality, and the 1B answer invented a retry_count field that is not in the notes.
Embeddings Are Cheap, Not Free
One guide lists bge-base-en-v1.5 at "~0" neurons. Per sentence that is fair: 0.10. But neurons scale with tokens, and 912 tokens in one batch cost 5.52. Embedding 1,000 pages of about 500 tokens would take roughly 3,000 neurons, a third of a day.
Images: the Meter Charged Three Times My Reading of the Table
The pricing page lists @cf/ at 4.80 neurons per 512 by 512 tile and 9.60 per step. The model returns a 1024 by 1024 JPEG, which is 4 tiles. I read that as 4 times 4.80 plus 4 steps times 9.60, or 57.60 neurons. The header said 172.80. At 1 step I expected 28.80 and got 57.60.
Both results fit one formula: the step rate is charged per tile too. Tiles times (4.80 plus 9.60 times steps) gives 4 times 43.20 = 172.80 at 4 steps and 4 times 14.40 = 57.60 at 1 step. A guide that measured on 2026-10-11 also reports 172.8 for a default image. Budget images from the table the way I first did and you run out three times sooner than planned.
What Failed First
gpt-oss-20b Returned Nothing at 256 Tokens
With max_tokens: 256, @cf/ used all 256 tokens on all three prompts and returned content: null each time, at a cost of 8.55, 17.22 and 40.91 neurons. A reasoning model writes its reasoning first (it comes back in reasoning_content), and that counts as output. With max_tokens at 1,024 it finished with finish_reason: "stop" after 380, 672 and 386 output tokens. The model page lists reasoning levels, but its input schema has no field to set one, so the token cap is the control you have.
Model Ids That Are Priced but Not in the Catalog
The pricing page lists @cf/ and llama-3.1-8b-instruct-fp8-fast. Neither was in the catalog on 2026-10-12, and both model pages returned 404. Both ids still answered. The table rate for llama-3.1-8b-instruct, 25,608 in and 75,147 out per million, gives 6.14 neurons for my 52-in, 64-out test. The header said 2.45, exactly what the fp8-fast rate gives. That is one call, so do not plan on it, but if you have the old id in your code, trust the header over the table.
A Typo in the Model Id
The docs list error 5007, "No such model", for an unknown model. A misspelled id, llama-3.1-8b-instruct-fp9, got something else:
HTTP 400 {"success":false,"errors":[{"code":7000,"message":"No route for that URI"}],"messages":[],"result":null}
That response had no cf-ai-neurons header. If you search your logs for 5007 to find bad model names, you will miss these.
Want more like this?
New posts in your inbox, a few a month.
Why Published Estimates Disagree by 10x
Search results for the free tier give numbers that are an order of magnitude apart. Two I checked on 2026-10-12:
- A free tier guide (last modified 2026-03-01) says 10,000 neurons is "roughly 1,300 LLM responses, about 12,500 embeddings, around 2,000 small image generations (example: FLUX.1 Schnell at 512×512)".
- Tool Freebie's Workers AI page (updated 2026-10-11) measured 20.4 neurons for a 579-token reply from
llama-3.1-8b-instruct, about 490 a day, and 172.8 per FLUX image, about 57 a day. The same page notes that it used to quote 15 to 25 calls a day.
My numbers sit between and around all of these, because "a response" has no fixed size. The output part of a 600-token answer costs ten times that of a 60-token answer. The 70B model cost 7 to 9 times the 1B model on the same prompts. 1,300 responses a day means 7.7 neurons each, which in my table falls between the 8B's short answer (3.44) and the 70B's short answer (9.78), and far below any long prompt. And 2,000 images a day is 5 neurons an image, which no FLUX setting I ran came close to. The FLUX.1 schnell schema on Workers AI has no size parameter, so I could not test 512 by 512.
The Script
The script reads a plan, a JSON array of calls, and appends one line per call to a CSV ledger: tokens, metered neurons, computed neurons and latency. Before each call it computes a worst case (prompt characters divided by 2 as tokens, plus the full max_tokens) and stops if that could pass STOP_AT. It never prints the token or the account id. The plan for my final check:
[
{"label":"granite S","kind":"chat","model":"@cf/ibm-granite/granite-4.0-h-micro","max_tokens":256,"messages":[{"role":"user","content":"What does an HTTP ETag header do? Answer in at most 80 words."}]},
{"label":"bge sentence","kind":"embed","model":"@cf/baai/bge-base-en-v1.5","text":["A client may retry the PUT to the same signed URL as long as the URL has not expired."]},
{"label":"flux 1 step","kind":"image","model":"@cf/black-forest-labs/flux-1-schnell","steps":1,"prompt":"a paper boat on a calm lake, flat illustration"}
]
In PowerShell, with a token that has Workers AI Read permission:
$env:CF_ACCOUNT_ID = "<ACCOUNT_ID>"
$env:CF_WORKERS_AI_TOKEN = "<TOKEN>"
node neurons.mjs plan.json ledger.csv --dry # worst case per call, no network
node neurons.mjs plan.json ledger.csv --show # run, print headers and answers
The final check, with the corrected FLUX formula, where computed and metered agree:
granite S: 200 in=47 out=74 header=0.82 computed=0.824 2257 ms
bge sentence: 200 in=25 out=0 header=0.15 computed=0.151 210 ms
flux 1 step: 200 in= out= header=57.60 computed=57.600 818 ms
The whole script:
// neurons.mjs: run Workers AI calls, record tokens, latency, the metered neurons and the neurons computed
// from the published rates, and refuse to start a call that could push the ledger past STOP_AT.
// Usage: node neurons.mjs <plan.json> <ledger.csv> [--show | --dry]
// Env: CF_ACCOUNT_ID, CF_WORKERS_AI_TOKEN (a token with Workers AI Read is enough for ai/run)
import fs from 'node:fs';
// Neurons per million tokens, copied from developers.cloudflare.com/workers-ai/platform/pricing/
// (page updated 2026-10-09, read 2026-10-12 UTC). Rates change: check the page before you trust these.
const RATES = {
'@cf/ibm-granite/granite-4.0-h-micro': { in: 1542, out: 10158 },
'@cf/meta/llama-3.2-1b-instruct': { in: 2457, out: 18252 },
'@cf/meta/llama-3.1-8b-instruct-fp8-fast': { in: 4119, out: 34868 },
'@cf/meta/llama-3.1-8b-instruct': { in: 25608, out: 75147 },
'@cf/meta/llama-3.1-8b-instruct-fp8': { in: 13778, out: 26128 },
'@cf/openai/gpt-oss-20b': { in: 18182, out: 27273 },
'@cf/meta/llama-3.3-70b-instruct-fp8-fast': { in: 26668, out: 204805 },
'@cf/baai/bge-base-en-v1.5': { in: 6058, out: 0 },
};
// @cf/black-forest-labs/flux-1-schnell: 4.8 per 512x512 tile and 9.6 per step. The meter charged the
// step rate per tile as well: neurons = tiles * (4.8 + 9.6 * steps).
const FLUX = { tile: 4.8, step: 9.6 };
const fluxNeurons = (tiles, steps) => tiles * (FLUX.tile + FLUX.step * steps);
const TOP_RATE = { in: 130000, out: 610000 }; // unknown model: assume a rate near the top of the table
const STOP_AT = 2400; // my own budget, not a Cloudflare limit
const [planFile, ledgerFile] = process.argv.slice(2);
const show = process.argv.includes('--show');
if (process.argv.includes('--dry')) {
const plan = JSON.parse(fs.readFileSync(planFile, 'utf8'));
for (const c of plan) console.log(c.label, worstCase(c).toFixed(1));
console.log('worst case total:', plan.reduce((s, c) => s + worstCase(c), 0).toFixed(1));
process.exit(0);
}
const { CF_ACCOUNT_ID: account, CF_WORKERS_AI_TOKEN: token } = process.env;
if (!account || !token) throw new Error('set CF_ACCOUNT_ID and CF_WORKERS_AI_TOKEN');
const hide = (s) => String(s).replaceAll(account, '<ACCOUNT_ID>').replaceAll(token, '<TOKEN>');
if (!fs.existsSync(ledgerFile)) fs.writeFileSync(ledgerFile, 'time_utc,model,label,status,input_tokens,output_tokens,neurons,basis,latency_ms,header_neurons,usage_neurons,computed_neurons\n');
const spent = () => fs.readFileSync(ledgerFile, 'utf8').trim().split('\n').slice(1)
.reduce((sum, line) => sum + Number(line.split(',')[6] || 0), 0);
// Worst case before the call: about 2 characters per token is pessimistic for English, plus template overhead.
function worstCase(c) {
if (c.kind === 'image') return fluxNeurons(4, c.steps ?? 4); // 1024x1024 = 4 tiles
const chars = JSON.stringify(c.messages ?? c.text).length;
const r = RATES[c.model] ?? TOP_RATE;
return ((chars / 2 + 100) * r.in + (c.max_tokens ?? 0) * r.out) / 1e6;
}
function jpegSize(b64) {
const b = Buffer.from(b64, 'base64');
for (let i = 2; i < b.length; ) {
const marker = b[i + 1], len = b.readUInt16BE(i + 2);
if (marker >= 0xc0 && marker <= 0xc3) return [b.readUInt16BE(i + 7), b.readUInt16BE(i + 5)];
i += 2 + len;
}
return null;
}
for (const c of JSON.parse(fs.readFileSync(planFile, 'utf8'))) {
const worst = worstCase(c);
if (spent() + worst > STOP_AT) {
console.log(`STOP: ${spent().toFixed(1)} spent + ${worst.toFixed(1)} worst case > ${STOP_AT}`);
break;
}
const body = c.kind === 'image' ? { prompt: c.prompt, steps: c.steps }
: c.kind === 'embed' ? { text: c.text }
: { messages: c.messages, max_tokens: c.max_tokens };
const url = `https://api.cloudflare.com/client/v4/accounts/${account}/ai/run/${c.model}`;
const t0 = performance.now();
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': 'neurons-measure/1.0' },
body: JSON.stringify(body),
});
const text = await res.text();
const ms = Math.round(performance.now() - t0);
let json = {};
try { json = JSON.parse(text); } catch {}
const u = json.result?.usage ?? {};
// Workers AI reports the neurons it charged in the cf-ai-neurons header (and, for some models, usage.neurons).
const header = res.headers.get('cf-ai-neurons') ?? '';
let inTok = '', outTok = '', computed = 0, basis;
if (!res.ok || json.success === false) {
basis = 'error';
console.log(`${c.label}: HTTP ${res.status} ${hide(text).slice(0, 300)}`);
} else if (c.kind === 'image') {
const [w, h] = jpegSize(json.result.image) ?? [1024, 1024];
const tiles = Math.ceil(w / 512) * Math.ceil(h / 512);
computed = fluxNeurons(tiles, c.steps ?? 4);
basis = `${w}x${h} ${tiles} tiles ${c.steps ?? 4} steps`;
} else if (u.prompt_tokens !== undefined) {
inTok = u.prompt_tokens; outTok = u.completion_tokens ?? 0;
const r = RATES[c.model] ?? TOP_RATE;
computed = (inTok * r.in + outTok * r.out) / 1e6;
basis = 'usage x rate';
} else {
computed = worst; basis = 'no usage field: worst case';
}
// The budget counts the metered figure when there is one, else the computed one, never less than computed.
const neurons = Math.max(Number(header) || 0, computed);
fs.appendFileSync(ledgerFile, [new Date().toISOString(), c.model, c.label, res.status, inTok, outTok, neurons.toFixed(3), basis, ms,
header, u.neurons ?? '', computed.toFixed(3)].join(',') + '\n');
console.log(`${c.label}: ${res.status} in=${inTok} out=${outTok} header=${header} computed=${computed.toFixed(3)} ${ms} ms`);
if (show) {
console.log(' headers:', [...res.headers.keys()].join(' '));
console.log(' result keys:', Object.keys(json.result ?? {}).join(' '), ' usage:', JSON.stringify(u));
const answer = json.result?.response ?? json.result?.choices?.[0]?.message?.content;
if (answer !== undefined) console.log(' answer:', hide(JSON.stringify(answer)).slice(0, 400));
const choice = json.result?.choices?.[0];
if (choice) console.log(' finish_reason:', choice.finish_reason, ' message keys:', Object.keys(choice.message ?? {}).join(' '),
' reasoning chars:', String(choice.message?.reasoning_content ?? choice.message?.reasoning ?? '').length);
}
}
console.log(`ledger total: ${spent().toFixed(1)} neurons`);
My first version computed FLUX from my reading of the table, so its worst case for an image was a third of the real charge. The guard held only because the stop sat far below 10,000. Compare the header with your own arithmetic on the first call of every new model.
What Happens at 10,000
I did not run past the allowance: on our Workers Paid account at XNDR that would simply bill. This is what the docs say, read on 2026-10-12:
- The allowance is 10,000 neurons a day on both Workers Free and Workers Paid, and all limits reset at 00:00 UTC.
- On Workers Free, the errors page lists code
3036, HTTP429: "You have used up your daily free allocation of 10,000 neurons. Please upgrade to Cloudflare's Workers Paid plan if you would like to continue usage." - On Workers Paid, usage above 10,000 is billed at $0.011 per 1,000 neurons. My whole test, at about 640 neurons, would have cost about $0.007 if it had been over the allowance.
- Per-minute request limits apply as well, 300 a minute for most text models.
How to Make 10,000 Neurons Last
- Log
cf-ai-neuronson every call. It covers text, images and embeddings. - Trim the prompt. With the 1,800-token prompt, input was 48 to 81 percent of each text model's bill.
- Pick the model id, not just the family. Llama 3 input rates on the table span 2,457 to 26,668 per million tokens.
- Cap output, but leave room for reasoning. 256 tokens is plenty for a Llama answer and nothing for
gpt-oss-20b. - Cache repeated questions. The AI Gateway post shows caching and a rate limit in front of Workers AI, and the template image post keeps image prompts short and fixed.
If you want an AI feature built on Cloudflare with its cost measured before launch, that is the kind of work on our services page.
FAQ
How many requests does the Workers AI free tier allow per day?
There is no request count, only 10,000 neurons a day. In my test on 2026-10-12 that was about 14,000 short answers from granite-4.0-h-micro, about 1,000 short answers or 127 long-prompt answers from llama-3.3-70b-instruct-fp8-fast, and 57 FLUX.1 schnell images at 4 steps.
How do I see how many neurons a Workers AI request used?
Read the cf-ai-neurons response header. It was on every successful call I made, text, embedding and image. Many models also return usage.neurons in the body. Neither is in the docs I read, so check it still exists before you build on it.
How many neurons does a FLUX.1 schnell image cost on Workers AI?
172.80 neurons for one 1024 by 1024 image at the default 4 steps, and 57.60 at 1 step, as metered on 2026-10-12. That fits 4 tiles times (4.80 plus 9.60 per step), which is three times what I first computed from the pricing table.
What happens when I go over 10,000 neurons on Workers AI?
According to the docs, on Workers Free further calls fail with error 3036 and HTTP 429 until the reset at 00:00 UTC. On Workers Paid, usage above the allowance is billed at $0.011 per 1,000 neurons. I did not run this myself.
Why did gpt-oss-20b return an empty answer on Workers AI?
Its reasoning counts as output. With max_tokens at 256, all three of my calls used every token on reasoning and returned content: null, and they were still charged. At 1,024 the same prompts finished with 380 to 672 output tokens.
Related Articles
- Cloudflare AI Gateway for Claude and OpenAI: Cache, Limit, Fall Back
- Prompt Injection-Safe AI Image Generation with Templates
- Building a Telegram-to-CLI Bridge for Claude Code
- XNDR services: edge APIs and AI features on Cloudflare.
Liked this one? Get the next in your inbox.
New posts and the odd product note, a few emails a month. Easy to leave.