Engineering4 min read

Cloudflare Error 1010: Your API Is Returning 403 to Python Clients and Nothing in Your Logs Shows It

Our OpenAI-compatible API answered curl, Node, python-requests and every SDK with 200, and Python's urllib with a 403 that never reached our servers. The cause was Cloudflare's Browser Integrity Check, which is right for web pages and wrong for an API. How to recognise error code 1010, how to prove it is the edge and not your app, and the Configuration Rule that turns the check off for /v1/ only.

The Paralon capybara as a gatekeeper lifting a barrier on a lane marked for robots while keeping the main gate closed

We were load-testing our own inference API from a small Python script and every request came back 403 Forbidden in 30 milliseconds. The same request from curl worked. The API key was fine. Nothing in our gateway logs, nothing in nginx. The requests were being refused before they reached us.

If your API sits behind Cloudflare and some clients get a 403 that your application never sees, this is probably the same thing. Here is how to tell in one command, and the fix that keeps Cloudflare's protection where it belongs.

The tell: error code: 1010

Ask for the headers:

curl -s -i -A "Python-urllib/3.12" https://paraloncloud.com/v1/models \
  -H "Authorization: Bearer prlc_…" | head -20
HTTP/2 403
server: cloudflare
cf-ray: a38506214b13f971-PRG
content-type: text/plain; charset=UTF-8

error code: 1010

Three things say it is the edge, not you:

  • server: cloudflare and a cf-ray header. Your origin's responses carry your server's header.
  • A plain-text body, error code: 1010, where your API would have returned JSON.
  • Nothing in your logs. The request was answered at Cloudflare's edge.

The number matters. Cloudflare's 1010 is specifically Browser Integrity Check: "the owner of this website has banned your access based on your browser's signature". Other 10xx codes are other features; 1010 is this one.

What Browser Integrity Check does, and why it hits APIs

It looks at the HTTP headers of a request and blocks ones that are commonly sent by abusive bots: a missing or odd User-Agent, missing headers a browser would always send. It is a reasonable default for a website. It is the wrong default for an API, because every API client is, by that definition, not a browser.

Which clients trip it is arbitrary from the developer's point of view. We measured, same key, same endpoint:

client User-Agentresult
curl/8.5.0200
python-requests/2.32.3200
node200
okhttp/4.12.0200
Python-urllib/3.12403, code 1010

The OpenAI Python SDK uses httpx and was never affected, which is why we did not notice for a while. A user with a twenty-line script on the standard library was.

The fix: turn it off for the API path only

Do not turn Browser Integrity Check off for the whole zone; it is doing useful work on your pages. Scope it with a Configuration Rule:

  1. Cloudflare dashboard, your zone, Rules → Configuration Rules → Create rule.
  2. Name: API: no browser integrity check.
  3. When incoming requests match: URI Path · starts with · /v1/.
  4. Then: find Browser Integrity Check in the settings list and set it to Off.
  5. Deploy.

It applies within seconds. Same request, after:

Python-urllib/3.12 on /v1/chat/completions   200
Python-urllib/3.12 on /v1/models             200
Python-urllib/3.12 on /                      403   ← still protected

That last line is the point: the site keeps the check, the API does not.

Finding the setting in the new dashboard

If you are looking at the redesigned Security section and cannot find it: Security → Settings, and type "Browser integrity" into the page filter. It is its own row, separate from the bot settings. The dashboard also shows "1 configuration rule" next to it once the path rule exists, which is a quick way to confirm the rule is attached.

What not to do

  • Do not enable Bot Fight Mode to "manage" this. On the Free plan it cannot be scoped by path or skipped by a custom rule, and it challenges exactly the traffic an API needs to accept: scripts, SDKs, and in our case the agents on GPU nodes that hold a WebSocket to the hub with a Go-http-client user agent. A challenge on that connection means nodes dropping off with no error they can act on.
  • Do not put the API on a different subdomain to escape the check if a Configuration Rule can do it. A path rule is one line and keeps one host.
  • Do not assume "no 403 in the logs" means nobody is getting one. Edge blocks are invisible to your application by construction.

Keep it from coming back

The failure is silent, so make it loud: a scheduled request with User-Agent: Python-urllib/3.12 against /v1/models, alerting on anything but 200. A CDN setting can change with a dashboard click; a probe catches it before a customer does. Ours is the one-line curl above, on a timer.

If you build against our API: nothing to change on your side. Any HTTP client works now, standard library included. If you find one that does not, tell us the cf-ray from the response header and we can see the block on our side.

Keep reading

Related Articles

The Paralon capybara at a Mac, holding a small file with a broken wax seal while a sealed copy glows on the desk
Engineering
4 min

"zsh: killed" on macOS After Re-downloading a Binary: the Code Signature Cache, and the One-Line Fix

A provider re-ran our Mac install command and every launch of the agent died instantly with "zsh: killed". The binary was fine, the same file ran on other Macs, and there was no crash log. The cause is how macOS caches code signatures per file: curl -o over an existing executable leaves one the kernel refuses to start. Here is how to recognise it, why it happens, and the fix for your install scripts.

macOSapple silicontroubleshooting
The Paralon capybara holding one glowing cable that splits toward three machines: a Linux tower with graphics cards, a Windows laptop, and a Mac
Engineering
10 min

One Model Name, Three Engines: vLLM on Linux, vLLM on WSL and llama.cpp on Apple Silicon Behind One Endpoint

The same model ID is now served by vLLM on Linux GPUs, vLLM on Windows/WSL, and llama.cpp on Apple Silicon Macs, and the caller cannot tell which one answered. Here is what had to be made equal for that to be true, the tokens-per-second we measured on an M1 Ultra and an M4 Max next to the GPUs, the 27B experiment that failed first and the rules it left behind, and a self-update bug that only exists when your inference server is a child process.

inferencellama.cppvllm
Three GPU nodes on a black field, each receiving a different emerald configuration path, with capacity bars of unequal height beneath them
Engineering
10 min

One Config Is Always the Weakest Card's Config

A mixed GPU fleet has one setting per model, and it has to be safe on the slowest machine in it. We measured what that costs: an RTX 5090 running at 8% of its memory bandwidth because a 24GB card three racks away needed a flag. Two arguments, applied only to the cards that could take them, made it fifteen times faster. Here is what the engine measures per node, what it changes, and the two things that surprised us.

engineeringbenchmarksinfrastructure