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.

BLOG-3195 1

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 happened and What 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 succeed
  • owner_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 retryable is true, wait retry_after and retry
  • if owner_action_required is true, 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:

  1. Agent blocked by a WAF rule (1020): the agent parses error_code, records ray_id, stops retrying, and escalates with useful context.
  2. 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.
  3. Rate-limited crawler (1015): the agent reads retryable and retry_after, applies backoff, and retries predictably.
  4. Developer debugging with curl: the exact agent view, including frontmatter and guidance, is reproducible without reverse-engineering HTML.
  5. RFC 9457-aware HTTP clients: any client dispatching on application/problem+json handles 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.