Home /

Cloudflare Workers AI free tier: keep editing when inference stops

Handle daily-allocation and temporary-capacity errors separately, with a server-side title-suggestion example that preserves the submitted note.

  • Workers Free stops inference at its daily allocation; Paid can bill additional usage.
  • Daily exhaustion and temporary capacity pressure share HTTP 429 but need different handling.
  • Local fixture tests check bounded retries and preserved input; no live inference was tested.

Cloudflare Workers AI's free tier includes 10,000 Neurons per day. On Workers Free, inference stops when that allocation runs out. On Workers Paid, usage beyond the allocation can be billed. Some models need a paid plan or prepaid credits even before that boundary. Those conditions come from Cloudflare's pricing page, checked on 27 October 2026.

For an optional feature, the useful question is what the person can still do when inference stops. A note editor can keep saving notes while its “Suggest a title” button is unavailable. That is the example here: a small server-side function with checked failure paths and an unchanged copy of the submitted note.

The checks use synthetic responses. They verify the request controller, not a live Cloudflare account, the quality of generated titles, or the moment a real allowance resets.

Two kinds of 429

Cloudflare's Workers AI error reference distinguishes these cases:

REST responseMeaningBehaviour in this example
HTTP 429, code 3036Daily free allocation exhaustedStop immediately; disable the feature for now
HTTP 429, code 3040Temporary capacity unavailableWait once, make one more attempt, then stop
HTTP 403, code 5035Model requires a paid planStop and fix the configuration
Unknown error or unreadable responseCause not establishedStop; keep the note available

The first two share an HTTP status. A generic “retry every 429” rule cannot tell them apart.

Cloudflare documents a daily reset at 00:00 UTC. That is an allowance reset time, not a guarantee that the next request will succeed. This example does not start a countdown or promise that the service will be available at midnight.

Save the note before asking for a title

Keep the note editor's save action independent of the AI request. Submit a snapshot of the saved note when someone asks for a suggestion, and show the returned title as a proposal they can accept or ignore.

The function below returns that same input with every completed result. It never replaces the note or selects a different model. For a real editor, also associate the request with the note's saved revision: a suggestion for revision 12 should not overwrite something the person has typed in revision 13.

This is a server-side example for Node.js 20 or later. An existing Cloudflare account, account ID and a suitably scoped Workers AI token are prerequisites for the remote request. Cloudflare documents the REST setup and token permissions. Keep those credentials out of browser code.

Save this as optional-title.mjs:

export const MODEL = "@cf/meta/llama-3.2-1b-instruct";

export async function suggestTitle(input, {
  accountId, token, fetchImpl = fetch,
  pause = ms => new Promise(resolve => setTimeout(resolve, ms))
} = {}) {
  if (typeof input !== "string" || !input.trim() || input.length > 2000)
    throw new Error("Use a non-empty note of at most 2,000 characters.");
  if (!/^[a-f0-9]{32}$/i.test(accountId ?? "") || !token)
    throw new Error("Server-side account ID and token are required.");

  const result = (state, message, suggestion = null) =>
    ({ state, message, suggestion, input });
  const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${MODEL}`;
  for (let attempt = 0; attempt < 2; attempt++) {
    let response, data;
    try {
      response = await fetchImpl(url, {
        method: "POST",
        headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
        body: JSON.stringify({
          messages: [
            { role: "system", content: "Suggest a short title for this note. Return only the title." },
            { role: "user", content: input }
          ],
          max_tokens: 64
        }),
        signal: AbortSignal.timeout(10000)
      });
      data = await response.json();
    } catch {
      return result("unavailable", "Title suggestion unavailable. Keep editing your note.");
    }

    const codes = Array.isArray(data?.errors)
      ? data.errors.map(error => Number(error?.code)) : [];
    if (response.status === 429 && codes.includes(3036))
      return result("daily-limit", "Today's AI allowance is used up. Your note is unchanged.");
    if (response.status === 403 && codes.includes(5035))
      return result("configuration", "This model needs a paid plan. Title suggestion is disabled.");

    if (response.status === 429 && codes.includes(3040)) {
      if (attempt === 0) { await pause(1000); continue; }
      return result("busy", "AI is busy. Keep editing and try again later.");
    }
    if (response.ok && data?.success === true &&
        typeof data.result?.response === "string" && data.result.response.trim())
      return result("ready", "Suggestion ready to review.", data.result.response.trim());
    return result("unavailable", "Title suggestion unavailable. Keep editing your note.");
  }
}

The chosen model, Llama 3.2 1B Instruct, is listed on the checked pricing page and supports the messages-based request shown here. Title quality has not been compared. Check the model's linked terms before using it.

The 2,000-character check is an application input limit. It is not a conversion to tokens or a promise about model context. The 64-token output cap keeps this particular request small. Neither turns Neurons into a fixed number of daily title suggestions.

From your server, call the function with the existing credentials:

import { suggestTitle } from "./optional-title.mjs";

const outcome = await suggestTitle(savedNote.text, {
  accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
  token: process.env.CLOUDFLARE_API_TOKEN
});

There is one automatic retry, only for the documented capacity code. Each attempt has a ten-second abort signal; the wait between attempts is one second. These timings are application choices for this optional feature. Errors with an unknown shape are deliberately left unclassified.

The function uses the REST response format. It does not parse thrown error-message strings from an env.AI binding, which is a different integration.

Keep the failure visible without blocking editing

Map the returned state to the existing editor:

  • ready: show the suggestion beside the saved note, with an explicit Apply action.
  • daily-limit: explain that today's AI allowance is exhausted and disable further suggestions for now.
  • busy or unavailable: leave editing and saving available; allow a later, deliberate retry.
  • configuration: disable suggestions and show an operator-facing configuration error.

Do not clear the editor when the request begins. A loading indicator should belong to the suggestion area, and accepting a title should still check that the relevant note revision is current.

The helper limits retries within one call. A public app also needs its own admission control so repeated clicks, concurrent users or several server instances do not each start a new attempt. After 3036, record the unavailable state in shared application state if you want all instances to stop probing the exhausted account. Reopening that gate later permits a request; it does not establish that capacity has recovered.

What was checked

Eight local tests exercise successful decoding, fixed-model requests, daily exhaustion, temporary capacity followed by success, persistent capacity failure, exhaustion on the retry, paid-only and unknown errors, malformed JSON, transport failure, and invalid input or credentials. Some tests contain several cases.

Every response is a labelled synthetic fixture. The checks confirm that daily exhaustion makes one request, persistent capacity failure makes two, and every completed outcome preserves the submitted note. They also confirm that invalid input makes no request. No live inference was called and no account allowance was deliberately exhausted.

A timeout fixture checks how a rejected request is handled. It does not measure a real ten-second network abort or prove that a provider stops work when the client disconnects.

That leaves the app-specific work: connect the function to authenticated, rate-limited server code, preserve saved revisions, and decide where its shared unavailable state lives. Keep the ordinary save path working throughout. A title suggestion can wait until the API is available again.

For the separate hosting decision, use the maintained free-compute guide.