Engineering

How Cell Runs on Cloudflare's Free Plan

How I added an SI guide to my site without a paid plan: a budget that fails closed, a Durable Object for the heavy work, and an edge Worker near 1ms of CPU.

On this page 27 sections

Cell is the SI guide in the corner of every page. It runs on Cloudflare’s Free plan and has never sent me a bill. It meters every question before it spends anything, does its heavy work in a Durable Object, and since October 2026 answers with Gemma 4 26B, which was charged about a quarter of the neurons Llama 3.3 70B was in the same 15-question eval.

Cell on the Free plan: an edge check, a neuron meter charged before each call, and Gemma 4 answering with Llama 3.3 70B behind itA three-panel schematic. Left: a question bubble passing a gate of the edge Worker. Center: a daily meter, a bar partly filled in copper, with a slot being charged before the call. Right: two model blocks, Gemma 4 26B in front and Llama 3.3 70B behind it, with an answer bubble. Copper arrows carry the question from panel to panel.ENGINEERING / CELLCharge every question before it runsCHECK · CHARGE · ANSWER01 CHECK02 CHARGE03 ANSWEREdge WorkerToday’s budgetLlamaGemma 4Workers AI

Llama stays behind it as a fallback. This is how each piece works, what changed from the first version, and what it took to move to the cheaper model.

What Cell is

Cell is the small NAND flash cell in the corner of every page. You ask it an SSD question and it answers from this site’s own writing, cites the pages it used, and says so when it falls back on general knowledge. The rest of the site is static files, so Cell is the only part that needs a server.

I gave myself one constraint: it has to run on the Cloudflare Free plan and never send me a bill. That constraint shaped everything else. Free gives you three limits that matter here.

  • Each Worker request gets 10ms of CPU.
  • The whole account gets 10,000 Workers AI neurons per UTC day. After that, every SI call fails. Free is never billed, so the risk is not cost. The risk is that the chat dies at 9 a.m. and stays dead.
  • The account can serve 100,000 Worker requests a day.

What Cell is

Three Free plan limits set the shape of Cell

Three panels side by side. CPU: 10ms of CPU per Worker request; Cell's edge Worker uses about 1ms. Neurons: 10,000 Workers AI neurons a day for the whole account; Cell's own budget is 8,500 a day. Requests: 100,000 Worker requests a day; a rate-limiting rule drops floods before they count.01 CPU10msof CPU perWorker requestEdge Worker:about 1ms02 NEURONS10,000neurons a dayfor the accountCell’s budget:8,500 a day03 REQUESTS100,000Worker requestsa dayFloods droppedbefore they countFree plan limitWhat Cell does about it
Figure 1 · The Workers Free plan limits that matter to Cell, and how Cell stays inside each one. Source: Cloudflare Workers limits and Workers AI pricing pages; our measurement and Cell’s config, October 26, 2026.

The shape of it

Each question travels through three pieces:

  1. The page sends the conversation and its own path to eyesonflash.com/api/assistant/chat. The Worker is routed on the site’s own domain, so the Content Security Policy keeps connect-src 'self' and there is no CORS to configure.
  2. The edge Worker does the cheap checks: origin, configuration, rate limit, body size and validation.
  3. A Durable Object named Cell does the expensive work. It searches the corpus, charges the budget, calls the model and streams the reply back through the Worker.

The shape of it

A question passes three pieces before it reaches the model

Four boxes left to right joined by copper arrows: the page posts the conversation and its path to its own domain; the edge Worker checks origin, rate limit, body size and validation in about 1ms of CPU; the Cell Durable Object searches, charges the budget, calls the model and streams the reply; Workers AI answers with Gemma 4 26B, or with Llama 3.3 70B once if Gemma sends no answer text. A dashed teal line carries the streamed reply back from right to left.01The pagePosts theconversationand its pathto its owndomain02Edge WorkerOriginRate limitBody sizeValidationabout 1ms CPU03CellDurable ObjectSearchCharge budgetCall the modelStream reply04Workers AIGemma 4 26BLlama 3.3 70Bonce, if Gemmasends noanswer textSame-origin route: no CORS, the page keeps connect-src ’self’One global Durable Object: an exact daily counter, a warm indexQuestionStreamed reply
Figure 2 · The cheap checks run in the edge Worker and the expensive work in one global Durable Object, so the Worker stays inside its CPU limit. Source: Cell’s code (workers/site-assistant) and our measurement, October 26, 2026.

That split exists because of the 10ms limit. Searching the whole site and streaming a model reply take far more than that. On one live question, Cell’s Durable Object used 604ms of CPU, so a Free Worker would be cut off. A Durable Object request gets 30 seconds of CPU by default. The edge Worker measures about 1ms per question, well inside its allowance.

One global Durable Object has a second benefit. Every question in the world passes through the same instance, so the daily counter is exact, and the search index stays warm between questions.

Where the answers come from

Each build generates /assistant-corpus.json from the guides, articles and project pages. Each chunk links to a real heading anchor. Cell fetches that file, indexes it with MiniSearch and searches on the latest question plus the one before it, so a follow-up such as “and how does that age?” keeps its topic. Results from the page the reader is on get a boost.

Where the answers come from

Every answer is built from this site’s own pages

Six numbered boxes in two rows joined by copper arrows. Top row: the build turns guides, articles and project pages into chunks; the corpus file assistant-corpus.json gives each chunk a heading link; Cell fetches it and builds a MiniSearch index that stays warm. Bottom row: search uses the latest question plus the one before, with the reader's page boosted; the top chunks go into the prompt as numbered sources with the rules; the answer cites them as [n] and the page renders them as same-site links.01 BUILDGuides, articlesand project pagesbecome chunks02 CORPUSassistant-corpus.json: each chunkhas a heading link03 INDEXCell fetches it andbuilds a MiniSearchindex, kept warm04 SEARCHLatest questionplus the one before;reader’s page boosted05 PROMPTTop chunks asnumbered sourceswith the rules06 ANSWERCites [n]; the pagelinks them, same-site paths onlyStepWhat the reader seesOrder
Figure 3 · The build writes a corpus of the site’s pages, Cell searches it for each question, and the model answers from what it found, citing each source. Source: Cell’s code (workers/site-assistant and the corpus build).

My prompt tells the model to cite sources as [n], to use the site’s own wording when a source defines a term, to flag general knowledge as such, and never to invent my opinions or numbers. The client renders citations as links, and it only accepts same-site paths.

Counting neurons before spending them

Workers AI bills in neurons, and the daily allowance is small, so the Worker meters itself. Before every model call, Cell estimates the worst-case cost and charges it against a daily budget kept in the Durable Object’s storage. If the charge doesn’t fit, the model is never called and the visitor sees “resting until tomorrow”.

Each estimate is an upper bound built from the real prompt:

  • Input tokens are the prompt length divided by 3 characters per token, plus a fixed allowance for each message’s chat-template framing. English text averages nearer 4 characters per token, so this over-counts on purpose.
  • Output is charged as the full reply limit, whether or not the model uses it. That limit was always 450 tokens until a Clef screen started sizing it per question.
  • Rates come from Cloudflare’s pricing page, per million tokens. Gemma 4 26B costs 9,091 neurons for input and 27,273 for output. Llama 3.3 70B, the fallback, costs 26,668 and 204,805.

A prompt of about 1,500 tokens on Gemma works out to roughly 14 neurons in and 12 out, so about 26 in total. In the eval I ran on October 26, 2026, Cell charged 31 to 43 neurons for each answered question, Clef screen included. Llama was charged 122 to 161 for the same questions that day. My budget is 8,500 of the 10,000, leaving 1,500 for the site’s article-maintenance Worker, so it now covers about 200 to 270 questions a day.

Counting neurons before spending them

A 1,500-token question: about 26 neurons on Gemma, 132 on Llama

Charged before the call: the whole prompt plus the full reply limit, at each model’s rate

Horizontal bars in neurons. Gemma 4 26B: 14 for the prompt in, 12 for the 450-token reply. Llama 3.3 70B: 40 for the prompt in, 92 for the reply. Totals about 26 and about 132.Model and part0255075100Neurons chargedGemma 4 26Bprompt in14Gemma 4 26B450-token reply12Llama 3.3 70Bprompt in40Llama 3.3 70B450-token reply92Prompt inReply reserved
Figure 4 · Neurons Cell charges up front for a 1,500-token prompt with 450 reply tokens reserved, split into input and output, on Gemma 4 26B and Llama 3.3 70B. Source: Cloudflare Workers AI pricing page, read October 26, 2026; our arithmetic.

Three details keep the meter honest:

The charge is one read-then-write on storage

A Durable Object holds its input gate shut across storage-only awaits, so two questions cannot both read the same balance and overspend.

The counter is one key per day

Each new UTC day’s first charge deletes everything else, so storage stays a single number.

Everything fails closed

Cell returns a 503 instead of making an unmetered call when the AI binding or the budget is missing, when the budget is not a number, or when the model has no rates in neurons.js. Setting the budget to "0" switches the chat off. Switching models can’t bypass the meter, because a new model has to have its rates added first.

There is also a backstop. If Workers AI reports that the daily allocation is used up anyway, Cell treats it as the resting state rather than an error. And once an edge isolate learns the budget is spent, it turns visitors away for the rest of the day without waking the Durable Object.

Keeping strangers from spending it

Any visitor-facing endpoint that spends a shared budget invites abuse, so the edge Worker is strict:

Keeping strangers from spending it

Five guards stand between a stranger and the neuron budget

Five stacked boxes joined top to bottom by copper arrows, each with a dashed arrow to the right to what happens on failure. One: the Cloudflare rate-limiting rule drops floods on the chat path. Two: the origin check lets only the site's own origins pass. Three: the per-IP limit allows 6 questions a minute, IPv6 keyed by its /64. Four: bounded input, a 32KB body, 500 characters a question and 8 turns. Five: the leak guard watches the reply for phrases from the prompt and replaces it.01Rate-limiting ruleCloudflare drops floods on the chat pathDropped02Origin checkOnly the site’s own origins passRefused03Per-IP limit6 questions a minute; IPv6 by /64Wait04Bounded input32KB body, 500 characters, 8 turnsRefused05Leak guardWatches the reply for prompt phrasesReplacedQuestion goes onQuestion stopped
Figure 5 · Each guard a question meets on its way to the model and back, and what happens to a question that fails it. Source: Cell’s code and config (workers/site-assistant), October 26, 2026.

Origin check

Only requests from the site’s own origins pass. It stops casual cross-site use and nothing more, so I never rely on it as authentication. There is no workers.dev or preview hostname to attack instead.

Per-IP rate limit

Each IP gets 6 questions a minute. IPv6 addresses are keyed by their /64, because a single visitor often controls a whole /64 and would otherwise rotate through addresses.

A Cloudflare rate-limiting rule

A Cloudflare rate-limiting rule (“Cell chat and MCP flood limit”) blocks more than 6 requests in 10 seconds from one address on the chat path before they count as Worker requests. Free allows one such rule, so since November 2026 the same rule also covers the site’s MCP server.

Bounded input

Cell reads the body as a stream and abandons it past 32KB. Questions stop at 500 characters and conversations at 8 turns and 6,000 characters. Only the page path is accepted from the browser’s page context. Page titles come from the corpus, so the client cannot write text into the system prompt.

Old assistant messages come back from the browser, so they can be forged. My prompt is followed by a reminder of the rules, and every chunk of source text has its angle brackets neutralized so it cannot close the <sources> block. A leak guard watches the reply as it streams. If the model starts reproducing distinctive phrases from its own prompt, the stream is cut and the client replaces what it showed with a short in-character refusal.

Privacy

Message content is never logged, only turn counts, source counts, the neuron estimate and timings. The conversation lives in the visitor’s sessionStorage, and tip preferences live in localStorage. The questions themselves do go to the Worker and to Workers AI to produce a reply.

Privacy

Logs keep counts and timings, never the words you type

Three panels. Browser: the conversation stays in sessionStorage and tip preferences in localStorage. Answering: your question goes to the Worker and to Workers AI to write a reply. Logs: turn count, source count, the neuron estimate and timings. Across the bottom, a crossed-out box: message text is never logged.01 BROWSERStays with youConversation insessionStorageTip preferencesin localStorage02 ANSWERINGSent to answerYour questiongoes to the Workerand to Workers AIto write a reply03 LOGSWhat is loggedTurn countSource countNeuron estimateTimingsNever logged: the text of any messageWhere it goesNever kept
Figure 6 · Where a conversation with Cell goes: what stays in your browser, what is sent to write the reply, and what the logs keep. Source: Cell’s code and config (workers/site-assistant and the widget).

Testing a chatbot

Two kinds of tests cover it. The Worker’s validation, budget, neuron math, streaming and hardening have Node tests that run with the rest of the site’s suite, using fakes for the AI binding and the Durable Object. Those catch arithmetic and plumbing mistakes without spending a neuron, and since the model change they replay real Workers AI streams I recorded.

My eval script, npm run eval:assistant, does the rest. It sends 19 questions to a live endpoint: five the site can answer, five general SSD questions, three off-topic requests, two jailbreak attempts and four questions readers asked that Cell got wrong. One jailbreak comes with a forged history. It flags missing citations and leaked prompt text, a reader question that does not cite the page that answers it, a known fact stated wrong, and sign-off lines. It spaces the requests to respect the rate limit. When it had 15 questions, one run on each model on October 26 charged 388 neurons on Gemma and 1,569 on Llama, and on Llama one more question got past the screen to the model. My run of all 19 on Gemma on November 5 charged 562. The flags are only a heuristic, so I read the answers myself after any change to the prompt, the model or the corpus.

Testing a chatbot

One eval run charged 388 neurons on Gemma and 1,569 on Llama

Our measurement: npm run eval:assistant, 15 questions, October 26, 2026, Clef screen included

Two horizontal bars in neurons charged for one 15-question eval run: Gemma 4 26B, the model now, 388; Llama 3.3 70B, the model before, 1,569.Model04008001,2001,600Neurons charged for one eval runGemma 4 26Bthe model now388Llama 3.3 70Bthe model before1,569Model nowModel before
Figure 7 · Neurons Cell charged for one run of its 15-question eval on each model, from the Worker’s own log lines. Source: Our measurement, October 26, 2026, Cell’s Worker log.

What changed from the first version

The version described above grew out of a first one. Two things changed once I checked it against the Free plan’s numbers.

What changed from the first version

The first cap let Cell spend 13 times the free neurons

A daily cap of 1,000 questions at about 130 neurons each on Llama 3.3 70B

Two horizontal bars in neurons a day. The first version's cap of 1,000 questions at about 130 neurons each allowed 130,000. The free allocation for the whole account is 10,000.Limit035,00070,000105,000140,000Neurons a dayFirst version’s cap1,000 questions × 130130,000Free allocationthe whole account10,000What the cap allowedWhat the account gets
Figure 8 · Neurons a day the first version’s question cap allowed, against the account’s free Workers AI allocation. Source: Cell’s first config (September 28, 2026) and the Workers AI pricing page; our arithmetic.

It counted questions, not neurons

My first budget was a daily question cap, defaulting to 1,000. That looks like a limit, but a question cost about 130 neurons on Llama, so 1,000 of them is about 130,000 neurons against an allowance of 10,000. The cap could never have protected the account, because the allowance would have run out long before the 1,000th question, and every SI call after that would have failed. The fix was to stop counting questions and count the thing Cloudflare actually meters: neurons, estimated from the real prompt before each call.

It did all the work in the edge Worker

Validation, corpus search, the budget check and the streamed reply all ran in one Worker. That’s fine on a paid plan, but indexing the corpus and streaming a reply take far more CPU than the ten milliseconds a Free request gets. Moving that work into a Durable Object took the edge Worker down to about 1ms per question.

That move had a side effect. My first design used one Durable Object for the budget and another for search. Once the work lived in a single object, both were redundant, so they merged into Cell. Migrations are append-only, so the config still records all of it. It shows two classes created, then both deleted and Cell created in their place. It also let me drop the CORS and preflight handling, since a same-origin route never needs it.

Retrieval took some tuning too, and the code comments record why:

  • Very short terms were matching everything. The “s” left over from “what’s” prefix-matched everything, so terms under two characters are ignored and prefix matching needs three.
  • Fuzzy matching turned “hire” into “wire”. It now applies only to longer terms, and only loosely.
  • Numbers are kept at any length, or “part 8” cannot be told from “part 9”.
  • Two neighboring chunks can each run past a thousand characters, so the limit on merged text had to fit both. Otherwise a section’s definition got dropped just when it was needed.
  • Readers type names with a space. “storage NEXT” searched for “storage” and “next” separately, and Part 8, “Where Memory Goes Next”, outranked the Storage-Next page. A hyphenated name such as Storage-Next or XL-FLASH is now also indexed as one joined word, and two neighboring words in a question are joined the same way, so the page that carries the name comes first.

One testing change was structural. Cell is a plain class with a fetch method rather than extending Cloudflare’s base class, so the tests can import it under Node without the Workers runtime. That is why the tests can cover the budget and the streaming path with fakes and no deployed Worker.

Moving to a cheaper model

For its first month, Cell answered with Llama 3.3 70B. Gemma 4 26B looked like a way out. Cloudflare prices its input at about a third of Llama’s and its output at about an eighth. I tried GLM 4.7 Flash beside it. In my first eval with them, every question that reached either model came back empty, with no error.

Moving to a cheaper model

With thinking on, the whole 300-token reply went to reasoning

Our measurement: raw Workers AI streams for one test question, October 26, 2026

Three horizontal bars in reply tokens. Gemma 4 26B with thinking on: 300, all reasoning, no answer. GLM 4.7 Flash with thinking on: 300, all reasoning, no answer. Gemma 4 26B with thinking off: 27, all answer.Model0100200300Reply tokens usedGemma 4 26Bthinking on300, no answerGLM 4.7 Flashthinking on300, no answerGemma 4 26Bthinking off27, answeredReasoningAnswer text
Figure 9 · Reply tokens each model used for the same question with a 300-token limit, and whether any of them were answer text. Source: Our measurement, October 26, 2026, recorded Workers AI streams.

Reasoning models think first

Both are reasoning models. I recorded their raw streams, and with a 300-token reply limit each spent all 300 tokens on its reasoning field and stopped at the limit before writing a word of answer. Cell’s reader already dropped that field, which is right, because thinking must never reach the visitor or the leak guard. The call was what had to change. For those two models only, Cell now sets the chat template’s switch that turns thinking off, and the same eval passed 15 of 15 on Gemma.

A fallback that pays its own way

Cloudflare could stop honoring that switch, so Llama 3.3 70B now stands behind Gemma. If a Gemma reply ends with no answer text, Cell charges the budget for one Llama call and asks the same prompt again. Past the budget, or if that call fails, the visitor gets the empty reply as before. A visitor who closes the tab while Gemma is still thinking costs no fallback call. In the eval it never fired, because every Gemma reply carried an answer.

A cheaper model that got facts wrong

In November 2026 I tried Qwen3 30B. On Cloudflare’s pricing page its input costs 4,625 neurons per million tokens, about half Gemma’s 9,091, and its output costs 30,475, a little more than Gemma’s 27,273. In my runs of the 19-question eval on November 5, it was charged 425 neurons, Clef screen included, against 562 for Gemma, and it passed every automatic check. Reading the answers changed the verdict. It said PCIe Gen5 runs at 16 GT/s per lane, citing my PCIe generations page, which gives 16 GT/s for Gen4. PCI-SIG gives 32 GT/s for Gen5. It also said QLC is “faster to read in some cases” than TLC, where the QLC page it cited says QLC’s longest array read takes about twice as long, and it said TRIM erases blocks. Gemma got all three right, so Gemma stays, and the PCIe question now fails the eval unless the answer says 32 GT/s.

The same reading caught something in Gemma. Its one allowed pun per reply had become a filler sentence such as “It is important to keep your WAF low so you don’t lose your charge too quickly”, and it would either cite a source for the pun, as it did there, or tag it as general knowledge, so a reader saw a citation or a flag on a line with no fact in it. Cell has stopped making puns, and its character now stays in its greeting and its plain wording.

What I’d tell someone copying this

What I’d tell someone copying this

Every model call is charged before it runs, the fallback too

A flow of boxes. Top row, joined by copper arrows: estimate the worst case, charge it to today's budget, Gemma 4 26B answers, stream the answer. From the charge box a dashed arrow labeled does not fit leads down to resting until tomorrow. From the Gemma box a dashed arrow labeled no answer text leads down to charge Llama, then a copper arrow to Llama 3.3 70B answers once. Under the estimate box: prompt length over 3 characters a token, plus the whole reply limit.01Estimate theworst case02Charge it totoday’s budget03Gemma 4 26Banswers04Stream theanswerResting untiltomorrow05No answer text:charge Llama06Llama 3.3 70Banswers oncedoes not fitno answerprompt ÷ 3 characters+ the whole reply limitA reader who leaves while Gemma thinks costs no Llama callNormal pathWhen it cannot go onWhat the reader gets
Figure 10 · Cell’s order for every question: estimate, charge, then call; a charge that does not fit ends the day, and the fallback is charged like any call. Source: Cell’s code (workers/site-assistant), October 26, 2026.

Budget the reply, not just the prompt

On Llama 3.3 70B, output tokens cost about 8 times as much as input tokens, so max_tokens is the lever that matters most. On Gemma 4 26B output costs about 3 times input, and a full prompt costs about as much as the reserved reply, so trimming the sources counts as much as the reply limit.

Meter before the call, and fail closed

A counter you check after the fact only reports what was spent. Charge first, and refuse when the charge doesn’t fit.

Move the heavy work to where the CPU limit is generous

Durable Objects fit well if you can live with one global instance.

Keep every miss as a test

When a reader gets a bad answer, the question goes into the eval with the page it should have cited. The next model, prompt or corpus change has to answer it correctly before it ships.

Test a cheaper model on the real stream

Changing the rate in the config takes one line, but the model behind it may not stream the way the old one did. Record its raw output before you trust the eval, and keep the model you trust behind it. Then read every answer, because a wrong number with a citation passes any check that only looks for citations.

Cell will sometimes tell a visitor it is resting until tomorrow, and that is what the budget is for. On the Free plan, the choice is between a guide that stops itself at a budget I set and one that runs into Cloudflare’s wall at some unknown moment. Because every call is charged before it runs, I can try a cheaper model on real traffic with its worst case already capped, and leave the chat running without watching it.

Sources

Primary is the original paper, standard or datasheet. Secondary is a review, a vendor summary or a news report. Measured is my own test data.

Show all 5 sources, 4 primary