Cloudflare turns error pages into machine-readable agent instructions
AI agents now account for billions of HTTP requests daily, yet when they hit errors they still receive HTML pages designed for human browsers. Those pages are large, ambiguous, and give agents no direction on whether to retry, back off, or escalate. Cloudflare is addressing this by returning RFC 9457-compliant structured error payloads in Markdown or JSON when agents request them.
The new behavior is live across the Cloudflare network and requires no configuration from site owners. Agents that send Accept: text/markdown, Accept: application/json, or Accept: application/problem+json receive a compact semantic contract instead of HTML. Browsers continue to see the same HTML error pages as before.
Why HTML error pages fail agents
Cloudflare-generated errors typically mean the edge is enforcing customer policy — access controls, bot rules, geo restrictions, or rate limits — rather than indicating an outage. Today those responses are rendered as HTML for humans:
<!DOCTYPE html>
<html>
<head>
<title>Access denied | example.com used Cloudflare to restrict access</title>
<style>/* 200 lines of CSS */</style>
</head>
<body>
<div class="cf-wrapper">
<h1 data-translate="block_headline">Sorry, you have been blocked</h1>
<!-- ... hundreds more lines ... -->
</div>
</body>
</html>
That markup gives an agent no reliable way to determine what error occurred, why it was blocked, or whether retrying will help. Custom Error Rules can customize many Cloudflare errors, but they depend on per-site configuration, so they cannot serve as a consistent machine contract across the web.
Structured, RFC 9457-compliant payloads
Cloudflare now returns structured responses for all 1xxx-class error paths — platform error codes covering edge-side failures like DNS issues, access denials, and rate limits. Both formats are live: Accept: text/markdown returns Markdown, Accept: application/json returns JSON, and Accept: application/problem+json returns JSON with the application/problem+json content type. The same contract will extend to Cloudflare-generated 4xx and 5xx errors next.
Markdown responses contain two parts:
- YAML frontmatter for machine-readable fields
- prose sections for explicit guidance (
What happenedandWhat you should do)
JSON responses carry the same fields as a flat object. The YAML frontmatter is the key automation layer: fields like error_code, error_name, and error_category classify the failure; retryable and retry_after drive backoff logic; owner_action_required tells the agent whether to escalate; and ray_id, timestamp, and zone make logs deterministic for support handoffs.
RFC 9457 — Problem Details for HTTP APIs — defines a standard JSON shape for error reporting, so clients can parse error responses without Cloudflare-specific code:
RFC 9457 member | What it contains |
|---|---|
type | A URI pointing to Cloudflare's documentation for the specific error code |
status | The HTTP status code (matching the actual response status) |
title | A short, human-readable summary of the problem |
detail | A human-readable explanation specific to this occurrence |
instance | The Ray ID identifying this specific error occurrence |
The operational fields — error_code, error_category, retryable, retry_after, owner_action_required, and related members — are RFC 9457 extension members; clients that don't recognize them simply ignore them.
Response shape and semantics
A rate-limit error (1015) in JSON:
{
"type": "https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1015/",
"title": "Error 1015: You are being rate limited",
"status": 429,
"detail": "You are being rate-limited by the website owner's configuration.",
"instance": "9d99a4434fz2d168",
"error_code": 1015,
"error_name": "rate_limited",
"error_category": "rate_limit",
"ray_id": "9d99a4434fz2d168",
"timestamp": "2026-03-09T11:11:55Z",
"zone": "<YOUR_DOMAIN>",
"cloudflare_error": true,
"retryable": true,
"retry_after": 30,
"owner_action_required": false,
"what_you_should_do": "**Wait and retry.** This block is transient. Wait at least 30 seconds, then retry with exponential backoff.\n\nRecommended approach:\n1. Wait 30 seconds before your next request\n2. If rate-limited again, double the wait time (60s, 120s, etc.)\n3. If rate-limiting persists after 5 retries, stop and reassess your request pattern",
"footer": "This error was generated by Cloudflare on behalf of the website owner."
}
The same error in Markdown:
---
error_code: 1015
error_name: rate_limited
error_category: rate_limit
status: 429
ray_id: 9d99a39dc992d168
timestamp: 2026-03-09T11:11:28Z
zone: <YOUR_DOMAIN>
cloudflare_error: true
retryable: true
retry_after: 30
owner_action_required: false
---
# Error 1015: You are being rate limited
## What Happened
You are being rate-limited by the website owner's configuration.
## What You Should Do
**Wait and retry.** This block is transient. Wait at least 30 seconds, then retry with exponential backoff.
Recommended approach:
1. Wait 30 seconds before your next request
2. If rate-limited again, double the wait time (60s, 120s, etc.)
3. If rate-limiting persists after 5 retries, stop and reassess your request pattern
---
This error was generated by Cloudflare on behalf of the website owner.
Both formats give an agent what it needs to classify the error, choose retry behavior, and decide whether escalation is required. The semantics differ deliberately across error families — a transient error like 1015 instructs wait-and-retry, while intentional blocks like 1020 or geographic restrictions like 1009 instruct the agent not to retry and to escalate instead.
Whether a client consumes Markdown or JSON, the operational meaning is identical: the same error identity, retry/backoff signals, and escalation guidance. Clients requesting application/problem+json receive application/problem+json; charset=utf-8; those requesting plain application/json receive application/json; charset=utf-8 with the same body.
Size and token efficiency
Structured responses are dramatically smaller than the HTML they replace. Measured for a live 1015 error response:
Payload | Bytes | Tokens (cl100k_base) | Size vs HTML | Token vs HTML |
|---|---|---|---|---|
HTML response | 46,645 | 14,252 | — | — |
Markdown response | 798 | 221 | 58.5x less | 64.5x less |
JSON response | 970 | 256 | 48.1x less | 55.7x less |
Both structured formats achieve roughly a 98% reduction in size and tokens versus HTML. Since agents often hit multiple errors in one workflow, these savings compound into lower model spend and faster recovery loops.
Error categories and control flow
Every 1xxx error maps to an error_category. Two fields make this operationally useful:
retryable— whether a retry can succeedowner_action_required— whether the problem must be escalated
Category | What it means | What the agent should do |
|---|---|---|
access_denied | Intentional block: IP, ASN, geo, firewall rule | Do not retry. Contact site owner if unexpected. |
rate_limit | Request rate exceeded | Back off. Retry after retry_after seconds. |
dns | DNS resolution failure at the origin | Do not retry. Report to site owner. |
config | Configuration error: CNAME, tunnel, host routing | Do not retry (usually). Report to site owner. |
tls | TLS version or cipher mismatch | Fix TLS client settings. Do not retry as-is. |
legal | DMCA or regulatory block | Do not retry. This is a legal restriction. |
worker | Cloudflare Workers runtime error | Do not retry. Site owner must fix the script. |
rewrite | Invalid URL rewrite output | Do not retry. Site owner must fix the rule. |
snippet | Cloudflare Snippets error | Do not retry. Site owner must fix Snippets config. |
unsupported | Unsupported method or deprecated feature | Change the request. Do not retry as-is. |
This replaces brittle if status == 429 heuristics with explicit control flow:
- if
retryableistrue, waitretry_afterand retry - if
owner_action_requiredistrue, stop and escalate - otherwise, fail fast without hammering the site
A minimal Python implementation:
import time
import yaml
def parse_frontmatter(markdown_text: str) -> dict:
# Expects: ---\n<yaml>\n---\n<body>
if not markdown_text.startswith("---\n"):
return {}
_, yaml_block, _ = markdown_text.split("---\n", 2)
return yaml.safe_load(yaml_block) or {}
def handle_cloudflare_error(markdown_text: str) -> str:
meta = parse_frontmatter(markdown_text)
if not meta.get("cloudflare_error"):
return "not_cloudflare_error"
if meta.get("retryable"):
wait_seconds = int(meta.get("retry_after", 30))
time.sleep(wait_seconds)
return f"retry_after_{wait_seconds}s"
if meta.get("owner_action_required"):
return f"escalate_owner_error_{meta.get('error_code')}"
return "do_not_retry"
Requesting structured errors
Send Accept: text/markdown, Accept: application/json, or Accept: application/problem+json. The behavior is deterministic — the first explicit structured type wins:
Accept header | Response |
|---|---|
application/json | JSON |
application/json; charset=utf-8 | JSON |
application/problem+json | JSON (application/problem+json content type) |
application/json, text/markdown;q=0.9 | JSON |
application/json, text/markdown | JSON (equal q, first-listed wins) |
text/markdown | Markdown |
text/markdown, application/json | Markdown (equal q, first-listed wins) |
text/markdown, */* | Markdown |
text/* | Markdown |
*/* | HTML (default) |
Wildcard-only requests (*/*) do not signal a structured preference. If the request succeeds, the origin returns normal content; the header only affects Cloudflare-generated error responses.
For quick testing, hit any Cloudflare-proxied domain at /cdn-cgi/error/1015 (or any other 1xxx code):
curl -s --compressed -H "Accept: text/markdown" -A "TestAgent/1.0" -H "Accept-Encoding: gzip, deflate" "<YOUR_DOMAIN>/cdn-cgi/error/1015"
Other error codes, JSON, and RFC 9457 Problem Details examples:
curl -s --compressed -H "Accept: text/markdown" -A "TestAgent/1.0" -H "Accept-Encoding: gzip, deflate" "<YOUR_DOMAIN>/cdn-cgi/error/1020"
curl -s --compressed -H "Accept: application/json" -A "TestAgent/1.0" -H "Accept-Encoding: gzip, deflate" "<YOUR_DOMAIN>/cdn-cgi/error/1015" | jq .
curl -s --compressed -H "Accept: application/problem+json" -A "TestAgent/1.0" -H "Accept-Encoding: gzip, deflate" "<YOUR_DOMAIN>/cdn-cgi/error/1015" | jq .
Initial use cases
Structured error responses help in several practical scenarios:
- Agent blocked by a WAF rule (
1020): the agent parseserror_code, recordsray_id, stops retrying, and escalates with useful context. - MCP (Model Context Protocol) tool hitting a geo restriction (
1009): the tool returns the machine-readable reason to the orchestrator, which can choose an alternate path. - Rate-limited crawler (
1015): the agent readsretryableandretry_after, applies backoff, and retries predictably. - Developer debugging with
curl: the exact agent view, including frontmatter and guidance, is reproducible without reverse-engineering HTML. - RFC 9457-aware HTTP clients: any client dispatching on
application/problem+jsonhandles Cloudflare errors without custom code.
Adoption guidance
For this to work across the web, agent runtimes should default to explicit structured Accept headers rather than bare */*. Use Accept: text/markdown, */* for model-first workflows and Accept: application/json, */* for typed control flow. Agent frameworks, SDKs, and browser automation stacks should treat bare */* as a legacy fallback.
This is the first layer of Cloudflare's agent infrastructure, which also includes AI Gateway for routing and observability, Workers AI for inference, and identity and access primitives for agent safety. Further details are available at agents.cloudflare.com.



