Create the Worker and bind Workers AI

The shell commands below use bash or zsh. Install uv and Node.js, then create the project directory with mkdir -p python-ai-endpoint/src and cd python-ai-endpoint. Install jq for the response checks. Use Cloudflare's pywrangler to develop and deploy the Worker. Its Python AI example calls the binding from a Default entrypoint. The binding needs no separate model SDK or Cloudflare REST API token, although Wrangler still needs an authenticated Cloudflare account.

python-ai-endpoint/
  pyproject.toml
  wrangler.jsonc
  src/entry.py

Put this in pyproject.toml. workers-py provides pywrangler; workers-runtime-sdk supplies Python Worker runtime types.

[project]
name = "python-ai-endpoint"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

[dependency-groups]
dev = ["workers-py", "workers-runtime-sdk"]

Put this in wrangler.jsonc. The python_workers flag selects the Python runtime. The AI binding becomes self.env.AI in the code below. Keep remote: true for local development: Cloudflare does not simulate Workers AI locally, and explicitly setting remote: false produces an error. The compatibility date shown is the date this example was reviewed.

{
  "name": "python-ai-endpoint",
  "main": "src/entry.py",
  "compatibility_date": "2026-09-22",
  "compatibility_flags": ["python_workers"],
  "ai": { "binding": "AI", "remote": true },
  "observability": { "enabled": true }
}

Accept a prompt only after the request passes the checks

Put the following in src/entry.py. The selected Llama 3.2 1B model accepts a prompt and max_tokens, then returns a synchronous JSON result with a response string. The 1,000-character input check and 64-token output cap are choices for this endpoint, not Cloudflare limits.

from urllib.parse import urlparse

from workers import Response, WorkerEntrypoint


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


class Default(WorkerEntrypoint):
    async def fetch(self, request):
        if urlparse(request.url).path != "/generate":
            return Response.json({"error": "not found"}, status=404)

        if request.method != "POST":
            return Response.json({"error": "use POST"}, status=405)

        token = getattr(self.env, "ENDPOINT_TOKEN", None)
        if not isinstance(token, str) or not token:
            return Response.json({"error": "ENDPOINT_TOKEN is not set"}, status=503)

        if request.headers.get("Authorization") != f"Bearer {token}":
            return Response.json({"error": "unauthorized"}, status=401)

        try:
            body = await request.json()
        except Exception:
            return Response.json({"error": "send valid JSON"}, status=400)

        prompt = body.get("prompt") if isinstance(body, dict) else None
        if not isinstance(prompt, str) or not prompt.strip() or len(prompt) > 1000:
            return Response.json(
                {"error": "prompt must contain 1 to 1000 characters"},
                status=400,
            )

        result = await self.env.AI.run(
            MODEL,
            {"prompt": prompt.strip(), "max_tokens": 64},
        )
        return Response.json(result)

The code returns 404 for another path, 405 for GET, 503 if the secret is unset, 401 for the wrong token, and 400 for malformed JSON or an invalid prompt. None of those branches calls AI.run(). Cloudflare's Python Worker basics document the request parsing, environment access, and JSON response methods used here.

The prompt check is not an HTTP body-size limit: request.json() parses the body before measuring prompt. Cloudflare's request body limit is far larger than the useful input for this endpoint. Keep direct callers on a backend you control. If you open the route to untrusted clients, limit the body before parsing it and control who may invoke the model. A token placed in browser JavaScript is visible to visitors.

Run the local endpoint and check the model field

Generate a caller token with python3 -c 'import secrets; print(secrets.token_urlsafe(32))'. Save it in .dev.vars next to wrangler.jsonc as ENDPOINT_TOKEN="your-generated-value", and add .dev.vars* to .gitignore. Cloudflare documents this local secret file. From the project directory, run uv run pywrangler dev, complete Cloudflare login if requested, and use the localhost URL it prints (often http://localhost:8787). Because the AI binding is remote, a valid local inference uses the real service and can consume your account's neurons (Workers AI limits).

On an account that has never deployed a Worker, remote development may stop with You need to register a workers.dev subdomain. That happened in the Free account used for this guide. Running uv run pywrangler deploy created its account subdomain; dev then connected. Cloudflare's CLI guide says first deployment may prompt for a subdomain. If you must deploy before finishing local checks, set the deployed secret as described below before sending authorized public requests. Until then, this example's /generate handler returns 503 for a POST because ENDPOINT_TOKEN is absent.

Run read -r -s TOKEN, paste the value from ENDPOINT_TOKEN, and press Return. This sets a shell variable without echoing the secret. Check a GET, an anonymous POST, and an authorized POST with an empty prompt. They should print 405, 401, and 400 respectively; none should invoke Workers AI.

curl -sS -o /dev/null -w 'GET: %{http_code}\n' \
  http://localhost:8787/generate

curl -sS -o /dev/null -w 'No token: %{http_code}\n' \
  -X POST http://localhost:8787/generate \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Say hello."}'

curl -sS -o /dev/null -w 'Empty prompt: %{http_code}\n' \
  -X POST http://localhost:8787/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":""}'

Now send one valid prompt. In bash or zsh, set -o pipefail makes the pipeline report an HTTP failure from curl --fail-with-body; jq -e also fails if the JSON lacks a nonempty response string. The test does not depend on the wording the model chooses.

set -o pipefail
curl --fail-with-body -sS http://localhost:8787/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Reply with one word for the color of a clear daytime sky."}' \
  | jq -e '.response | select(type == "string" and length > 0)'

A nonempty response shows that the local route returned model output; inspect the text before using it. If the three rejection statuses differ, check the URL, method, .dev.vars placement, and Authorization header. If only the valid call fails, check the AI binding, Cloudflare login, model access, and upstream error before changing the prompt.

During the September 22, 2026 local check, the first empty-prompt request returned HTTP 500 with Network connection lost from the remote preview proxy. The first model request also returned 500 with an InferenceUpstreamError reference. Repeating those requests returned the expected 400 and a 200 with a nonempty response. A proxy or upstream 500 does not verify an input-validation branch or prove the daily neuron allowance ran out. Keep the error reference, inspect the dev log, and retry once before changing the Worker.

Deploy, set the production secret, and verify the public route

Run uv run pywrangler deploy and copy the workers.dev URL reported by the command. The deployed code initially returns 503 for /generate until ENDPOINT_TOKEN exists in that Worker. From the same project directory, run uv run pywrangler secret put ENDPOINT_TOKEN and enter the value at its prompt. Cloudflare documents that pywrangler supports Wrangler commands and that secret put immediately deploys a new Worker version. Keep the value out of wrangler.jsonc and Git.

Set ENDPOINT to the exact public base URL printed by deployment, without a trailing slash, and set TOKEN to the deployed secret with read -r -s TOKEN. The first command should print No token: 401; the second should return a nonempty model response.

curl -sS -o /dev/null -w 'No token: %{http_code}\n' \
  -X POST "$ENDPOINT/generate" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Say hello."}'

set -o pipefail
curl --fail-with-body -sS "$ENDPOINT/generate" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Say hello."}' \
  | jq -e '.response | select(type == "string" and length > 0)'
Diagram of four endpoint checks: anonymous POST returns 401, empty prompt returns 400, valid inference returns a nonempty response, and dashboard usage is checked separately
Each check establishes a different layer: access control, input rejection, returned model output, and account-level usage. Credit: Grow AI Skills original conceptual diagram. Open full-size image.

In the September 22 public check, the deployed Worker returned 404 for another path, 405 for GET, 401 without the token, and 400 for an authenticated empty prompt. One authorized request returned HTTP 200 with response: "Blue" and usage showing 57 prompt tokens, two completion tokens, and about 0.1766 neurons. The Workers AI dashboard later displayed 0.43 of 10,000 neurons used that day, including other local and public calls in this check. That account-level counter cannot be attributed to the single public request; its own usage.neurons field is the narrower observation. Check the Worker's request metrics and Workers Logs if your public response differs from the local one.

Cloudflare Workers AI dashboard showing 0.43 of 10,000 daily neurons used and 0.43 neurons for Llama 3.2 1B in the last 24 hours
Workers AI usage after the local and public checks on September 22, 2026. The 0.43-neuron account total includes several calls; the single public response reported about 0.1766 neurons. Credit: Grow AI Skills screenshot from the owner-provided Cloudflare Free account. Open full-size image.

Estimate what the free plan will actually allow

Comparison of Workers Free daily request allowance of 100000 and Workers AI daily allocation of 10000 neurons, with a separate 10 millisecond CPU limit per request
Worker requests, AI neurons, and Worker CPU have different meters. The first exhausted limit determines what fails. Credit: Grow AI Skills original conceptual diagram. Open full-size image.

Both daily allowances reset at 00:00 UTC (Workers limits; AI pricing). A backend stuck retrying a 401 can raise the Worker request counter while the neuron counter stays flat; fix its token or retry logic. The Free plan also allows 10 ms of Worker CPU per HTTP request. Waiting for the remote AI result is wall time, while parsing a large body or running extra Python code uses CPU and can hit that limit.

For this model, Cloudflare currently lists 2,457 neurons per million billed input tokens and 18,252 per million billed output tokens. For a hypothetical call billed for 100 input tokens and 64 output tokens, the arithmetic is (100 × 2457 + 64 × 18252) / 1,000,000 = 1.413828 neurons. That is an illustration using published unit rates, not a measured request or a guaranteed per-call cost. Actual token counts, output length, other account usage, and current model rates determine how many requests fit under 10,000 neurons. A 64-token max_tokens cap limits requested output length; it does not set a fixed bill. Read the current model pricing row and your dashboard usage before forecasting volume.

Cloudflare's default text-generation rate limit is 300 requests per minute unless a model has a different paid-model limit. A burst can fail before the daily neuron allowance is spent. If several backend jobs share this endpoint, the single Bearer token cannot identify which job used the neurons; record usage per job in your own backend if that distinction matters.

Diagnose the failure you actually received

The example leaves AI.run() exceptions uncaught. Its own 400, 401, 404, 405, and 503 JSON responses are easy to recognize, but an upstream AI error may surface through an outer Worker failure with a different HTTP status. Inspect Workers Logs for an upstream code or message; if one appears, compare it with Cloudflare's Workers AI error list. A 429 alone does not establish that the neuron allocation is exhausted.

Signal in your responseMeaning and next check
Example JSON 503: ENDPOINT_TOKEN is not setThe secret is missing from the active Worker version. Run pywrangler secret put ENDPOINT_TOKEN for the intended Worker and retry after deployment.
Example JSON 401 or 400The request failed authentication or input validation before AI.run(). Compare the header or JSON with the commands above; there is no reason to debug model capacity yet.
Workers AI internal 5016, upstream 403Cloudflare reports that the account has not agreed to the Llama 3.2 model terms. Review the model's terms and license and the account's agreement requirement before retrying.
Workers AI internal 5035, upstream 403If you changed MODEL, the replacement may require Workers Paid or configured prepaid AI Gateway credits. Check current model eligibility; increasing the Worker request allowance does not unlock a restricted model.
Workers AI internal 3036, upstream 429The account exhausted its daily free neurons. Inspect Workers AI usage and wait for the 00:00 UTC reset on Free; reduce token use to avoid the same failure the next day.
Workers AI internal 3040, upstream 429The model has insufficient capacity at that moment. Retry later with backoff; this code alone does not show that the daily neurons ran out.
Cloudflare error 1027The Workers Free account reached 100,000 daily Worker requests. Check Worker request metrics; the counter resets at midnight UTC.
Cloudflare error 1102 or invocation outcome exceededCpuThe Worker exceeded its CPU budget. Inspect CPU metrics and any extra parsing or Python work you added. Waiting for AI is wall time; code execution uses CPU.
Cloudflare 403 with body error code: 1010Cloudflare identifies this as a browser-signature block before your Worker responds. In this check, Python's default urllib request received 1010. Giving that client a descriptive User-Agent reached the Worker and returned its JSON 401, as did curl. Check your client's headers and Cloudflare security controls before debugging the Bearer token or model.

If you later add error handling for client applications, return a non-2xx status for inference failures and preserve the upstream error code in private logs. Returning {"response":""} with HTTP 200 would make a failed AI call look like a valid empty answer. Avoid logging prompt text or the Bearer token during this investigation.

Know where this small Python design stops fitting

Cloudflare runs Python Workers through Pyodide inside a V8 isolate. A Python library that works on a conventional Linux server may fail to deploy here: Cloudflare supports pure Python packages, PyEmscripten wheels, and included Pyodide packages, while some native dependencies have no compatible wheel (package support). Check package compatibility before building this endpoint around a local machine-learning stack. This example uses a hosted model, so no model weights or GPU runtime are bundled in the Worker.

Python Worker file writes go to an ephemeral, in-memory filesystem. If the endpoint must retain results or per-caller usage between requests, attach durable storage instead of writing a local file. Browser clients need authentication and limits for individual users because they cannot keep this shared token private.