Skip to content

Rate limits and challenge

The defense posture sets the baseline; the objects below tune it per service: limits per source and for the whole service, the proof-of-work challenge, slow-HTTP timeouts and threat-intelligence lists. Their place in the request path: pipeline. Limits for single paths are protection rules with a rate. Requests need API access.

Endpoints

Method Path Key Purpose
PATCH /services/{fqdn} write Change a vService

PATCH /services/{fqdn}

Body: ServicePatch
Returns: 200, Service

Request: waf_and_limit
{
  "waf": {
    "enabled": true
  },
  "rate_limiting": {
    "req_per_10s": 800
  }
}
Request: replace_origins
{
  "origins": [
    {
      "name": "origin-1",
      "address": "192.0.2.10",
      "port": 443
    }
  ]
}
Answer 200: waf_and_limit
{
  "fqdn": "a.example.com",
  "tenant": "tenant-a",
  "enabled": true,
  "mode": "https",
  "ports": [
    443
  ],
  "udp_ports": [],
  "created": "2026-10-01T00:00:00+00:00",
  "created_by": "",
  "account_id": "acc-a",
  "network": {
    "vips": [
      {
        "anycast_ipv4": "203.0.113.1",
        "anycast_ipv6": "2001:db8:a::1"
      }
    ],
    "pool": null,
    "edge_hostname": "a-example-com.edge.protect.zeroservices.eu"
  },
  "network_protection": {
    "enabled": true
  },
  "backend": {
    "algorithm": "roundrobin",
    "health_check": {}
  },
  "origins": [
    {
      "name": "origin-1",
      "address": "192.0.2.10",
      "port": 443,
      "tls": true,
      "ssl_verify": true,
      "weight": 100,
      "maxconn": 100
    }
  ],
  "waf": {
    "enabled": true,
    "fail_open": true,
    "mode": "On",
    "paranoia_level": 1,
    "inbound_threshold": 5,
    "disabled_rules": []
  },
  "rate_limiting": {
    "challenge_req_per_10s": 200,
    "req_per_10s": 800,
    "conn_per_10s": 50,
    "conn_drop_per_10s": 500,
    "path_default": 1000,
    "path_aggregate_per_10s": 5000,
    "geo_tcp_reject_sess_rate": 200,
    "ja4_enabled": false,
    "ja4_req_per_10s": 100,
    "ja4_action": "challenge"
  },
  "pow": {
    "enabled": true,
    "difficulty": 18,
    "cookie_ttl": 86400,
    "always_challenge": false,
    "exempt_crawlers": true,
    "bind_ip": true,
    "botcheck_per_10s": 1000
  },
  "admission": {
    "enabled": false,
    "credit_required": 5,
    "credit_window": "60s",
    "known_ttl": "24h",
    "unknown_conn_rate": 1500,
    "table_size": "1m"
  },
  "ssl": {
    "mode": "acme",
    "acme_provider": "zerossl",
    "acme_email": null,
    "https_redirect": false,
    "cert_status": "none",
    "cert_issued": null,
    "cert_expires": null,
    "cert_last_renewal": null,
    "days_remaining": null
  },
  "aliases": [
    {
      "fqdn": "www.a.example.com",
      "ssl": {
        "mode": "acme",
        "acme_provider": "zerossl",
        "acme_email": null,
        "https_redirect": false,
        "cert_status": "none",
        "cert_issued": null,
        "cert_expires": null,
        "cert_last_renewal": null,
        "days_remaining": null
      }
    }
  ],
  "cache": {
    "enabled": true
  },
  "slow_http": {
    "enabled": true,
    "timeout_http_request": "5s",
    "timeout_client_fin": "5s"
  },
  "tcp_timeout": {
    "client": "30s",
    "server": "30s",
    "tunnel": "30s"
  },
  "udp_timeout": {
    "idle": "300s",
    "unreplied": "30s",
    "unreplied_pkts": 32,
    "src_pps": 0
  },
  "sni_policy": "allowlist",
  "proxy_protocol": false,
  "tls_termination": false,
  "priority": {
    "enabled": true,
    "session_signal": []
  },
  "response_headers": {
    "hsts": {},
    "cors": {},
    "x_frame_options": null,
    "x_content_type_options": false,
    "referrer_policy": null
  },
  "content_negotiation": {
    "enabled": false,
    "encodings": [
      "zstd",
      "gzip"
    ],
    "file_types": [
      "css",
      "js",
      "html"
    ]
  },
  "logging": {
    "enabled": false
  },
  "threat_intel": {
    "mode": "log",
    "abuse": "log",
    "tor": "log"
  },
  "redirects": [],
  "origin_host": null,
  "origin_sni": null,
  "maintenance_mode": false,
  "auto_mitigation": false,
  "defense_posture": null,
  "protection_allowlist": null,
  "edges": [],
  "protection_rules": [],
  "append_trailing_slash": false,
  "exceed_no_rtbh": false,
  "exceed_no_flowspec": false,
  "origin_kind": "dynamic",
  "alias_limit": 20,
  "serving_edges": [
    "shared-1"
  ],
  "posture_drift": [],
  "baseline": null
}
Answer 200: replace_origins
{
  "fqdn": "a.example.com",
  "tenant": "tenant-a",
  "enabled": true,
  "mode": "https",
  "ports": [
    443
  ],
  "udp_ports": [],
  "created": "2026-10-01T00:00:00+00:00",
  "created_by": "",
  "account_id": "acc-a",
  "network": {
    "vips": [
      {
        "anycast_ipv4": "203.0.113.1",
        "anycast_ipv6": "2001:db8:a::1"
      }
    ],
    "pool": null,
    "edge_hostname": "a-example-com.edge.protect.zeroservices.eu"
  },
  "network_protection": {
    "enabled": true
  },
  "backend": {
    "algorithm": "roundrobin",
    "health_check": {
      "method": "GET",
      "path": "/",
      "host": null,
      "interval": "10s",
      "rise": 2,
      "fall": 3,
      "expect": null
    }
  },
  "origins": [
    {
      "name": "origin-1",
      "address": "192.0.2.10",
      "port": 443,
      "tls": true,
      "ssl_verify": true,
      "weight": 100,
      "maxconn": 100
    }
  ],
  "waf": {
    "enabled": true,
    "fail_open": true,
    "mode": "On",
    "paranoia_level": 1,
    "inbound_threshold": 5,
    "disabled_rules": []
  },
  "rate_limiting": {
    "challenge_req_per_10s": 200,
    "req_per_10s": 800,
    "conn_per_10s": 50,
    "conn_drop_per_10s": 500,
    "path_default": 1000,
    "path_aggregate_per_10s": 5000,
    "geo_tcp_reject_sess_rate": 200,
    "ja4_enabled": false,
    "ja4_req_per_10s": 100,
    "ja4_action": "challenge"
  },
  "pow": {
    "enabled": true,
    "difficulty": 18,
    "cookie_ttl": 86400,
    "always_challenge": false,
    "exempt_crawlers": true,
    "bind_ip": true,
    "botcheck_per_10s": 1000
  },
  "admission": {
    "enabled": false,
    "credit_required": 5,
    "credit_window": "60s",
    "known_ttl": "24h",
    "unknown_conn_rate": 1500,
    "table_size": "1m"
  },
  "ssl": {
    "mode": "acme",
    "acme_provider": "zerossl",
    "acme_email": null,
    "https_redirect": false,
    "cert_status": "none",
    "cert_issued": null,
    "cert_expires": null,
    "cert_last_renewal": null,
    "days_remaining": null
  },
  "aliases": [
    {
      "fqdn": "www.a.example.com",
      "ssl": {
        "mode": "acme",
        "acme_provider": "zerossl",
        "acme_email": null,
        "https_redirect": false,
        "cert_status": "none",
        "cert_issued": null,
        "cert_expires": null,
        "cert_last_renewal": null,
        "days_remaining": null
      }
    }
  ],
  "cache": {
    "enabled": true
  },
  "slow_http": {
    "enabled": true,
    "timeout_http_request": "5s",
    "timeout_client_fin": "5s"
  },
  "tcp_timeout": {
    "client": "30s",
    "server": "30s",
    "tunnel": "30s"
  },
  "udp_timeout": {
    "idle": "300s",
    "unreplied": "30s",
    "unreplied_pkts": 32,
    "src_pps": 0
  },
  "sni_policy": "allowlist",
  "proxy_protocol": false,
  "tls_termination": false,
  "priority": {
    "enabled": true,
    "session_signal": []
  },
  "response_headers": {
    "hsts": {
      "enabled": false,
      "max_age": 31536000,
      "include_subdomains": false,
      "preload": false
    },
    "cors": {
      "enabled": false,
      "rules": []
    },
    "x_frame_options": null,
    "x_content_type_options": false,
    "referrer_policy": null
  },
  "content_negotiation": {
    "enabled": false,
    "encodings": [
      "zstd",
      "gzip"
    ],
    "file_types": [
      "css",
      "js",
      "html"
    ]
  },
  "logging": {
    "enabled": false
  },
  "threat_intel": {
    "mode": "log",
    "abuse": "log",
    "tor": "log"
  },
  "redirects": [],
  "origin_host": null,
  "origin_sni": null,
  "maintenance_mode": false,
  "auto_mitigation": false,
  "defense_posture": null,
  "protection_allowlist": null,
  "edges": [],
  "protection_rules": [],
  "append_trailing_slash": false,
  "exceed_no_rtbh": false,
  "exceed_no_flowspec": false,
  "origin_kind": "dynamic",
  "alias_limit": 20,
  "serving_edges": [
    "shared-1"
  ],
  "posture_drift": [],
  "baseline": null
}

Fields

Part of the service: send them with PATCH /services/{fqdn}; inside an object, send only the keys you change.

Field Type Notes
defense_posture string one of off, standard, under_attack; may be null; Protection bundle for the service type. off: loose limits only, no challenge, WAF or threat intelligence. standard: denies the network reputation list and, for browser services, challenges busy login and admin paths. under_attack: strictest, challenges every request where the type allows proof-of-work. Setting it applies the bundle's values to rate_limiting, pow, slow_http, cache, waf, threat_intel, admission and priority and replaces the rules it added
sensitivity string one of low, medium, high; may be null; Learn the service-wide limits (path_aggregate_per_10s, geo_tcp_reject_sess_rate, challenge answers, origin maxconn) from your normal peak: low 3 times, medium 2 times, high 1.5 times, never below the floor. Null: the fixed values apply. HTTPS only
size_tier string one of S, M, L, XL; may be null; S, M, L or XL: scales the floors of the learned limits by 0.5, 1, 2 or 4. Only with a sensitivity
auto_mitigation boolean may be null; true: automatic attack response may add time-limited protection rules to the vService itself, never against your allowlist. false: it only suggests them
rate_limiting RateLimitingPatch may be null; Rate limits
pow PowPatch may be null; Proof-of-work challenge
admission AdmissionPatch may be null; Returning-visitor admission
slow_http SlowHttpPatch may be null; Timeouts against slow requests
threat_intel ThreatIntelPatch may be null; Reputation lists

Postures

defense_posture Baseline
off no baseline rules, no fingerprint limit
standard login paths 10 and admin paths 20 requests per 10 s, challenge above; 1000 requests per fingerprint per 10 s, challenge above
under_attack the most aggressive baseline, for the duration of an incident; 200 requests per fingerprint per 10 s, challenge above
sensitivity Service-wide limits
null (off, default) the posture's values
low 3× your normal peak
medium 2× your normal peak
high 1.5× your normal peak

RateLimiting

Field Type Notes
challenge_req_per_10s integer 1–100000; default 200; HTTP requests per 10 s a source may send before the edge asks it for proof-of-work; static files and crawlers excepted. Only with pow.enabled
req_per_10s integer 1–100000; default 500; HTTP requests per 10 s a source may send; above it the edge answers 429
conn_per_10s integer 1–100000; default 50; New connections per 10 s a source may open; above it its HTTP requests get 429, on tcp and anyconnect vServices the connection is refused. Also the new forwarded UDP flows per 10 s a source may open
conn_drop_per_10s integer 1–1000000; default 500; New connections per 10 s from one source above which the edge drops its further connections silently, before TLS; no rule exempts from it. Must be above conn_per_10s. Also sets the layer 3/4 SYN limit per source, at one fifth of this per second
path_default integer 1–100000; default 1000; HTTP requests per 10 s a source may send to the vService, all paths together, before a further request gets 429. It is compared with the source's total rate, not with its requests to one path
path_aggregate_per_10s integer 0–10000000; default 5000; Responses not served from the cache per 10 s for one hostname and path (without query string), all sources together. Above it, and only while your origins are under load (80 % of maxconn in use, or a queue), requests get proof-of-work, or 429 for static files, crawlers and without pow.enabled; solved clients and sources with returning-visitor credit pass. 0: off
geo_tcp_reject_sess_rate integer 0–1000000; default 200; Only with a geofence rule (a negated deny rule on geo, without rate): while the vService takes more new sessions per second than this on an edge, sources the geofence does not allow are refused at connection setup instead of getting a 403. 0: always answer 403
ja4_enabled boolean default false; Limit requests per JA4 TLS fingerprint
ja4_req_per_10s integer 1–100000; default 100; HTTP requests per 10 s per JA4 fingerprint, all sources together. A fingerprint is shared by everyone running the same software
ja4_action string one of challenge, deny; default "challenge"; Above ja4_req_per_10s: challenge (proof-of-work, only with pow.enabled) or deny (429)
Field Counted per Above the limit
conn_drop_per_10s source connection dropped silently, before TLS
conn_per_10s, req_per_10s source 429
challenge_req_per_10s source proof-of-work challenge
path_default source, all its requests, against the limit of the requested path 429
path_aggregate_per_10s hostname and path, all sources together challenge, or 429 without proof-of-work; 0 is off
ja4_req_per_10s TLS fingerprint, all sources together ja4_action
geo_tcp_reject_sess_rate the service, sessions per second sources outside your country allowlist are rejected at connection setup instead of getting a 403; 0 always answers

Pow

Field Type Notes
enabled boolean default true; Proof-of-work challenge. While your origins are under load (80 % of maxconn in use, or a queue), unknown visitors asking for HTML get it too. Off: no challenge of any kind happens
difficulty integer 16–20; default 18; Difficulty of the puzzle, 16 to 20: each step doubles the work, 18 is about a thousand hashes in the browser. A solution counts only on this vService and at this difficulty
cookie_ttl integer 60–604800; default 86400; Seconds a solved challenge lets the visitor through, 60 to 604800; the puzzle itself must be solved within the same time
always_challenge boolean default false; Challenge every request without a solved puzzle, except static files, crawlers while exempt_crawlers is on, and sources an allow or skip rule exempts. Lockdown
exempt_crawlers boolean default true; Never challenge known crawlers (GET /reference/crawlers), recognised by their published source networks
bind_ip boolean default true; A solved puzzle holds only for the solver's IPv4 /24 or IPv6 /64
botcheck_per_10s integer 100–100000; default 1000; read-only; Set by support on request

Admission

Field Type Notes
enabled boolean default false; Sources that earned credit with successful answers over time pass. While the edge is under strain, new connections from all other sources share one budget and are refused above it, before the TLS handshake
credit_required integer 1–1000; default 5; read-only; Set by support on request
credit_window string default "60s"; read-only; Set by support on request
known_ttl string default "24h"; read-only; Set by support on request
unknown_conn_rate integer 1–10000000; default 1500; read-only; Set by support on request
table_size string default "1m"; read-only; Set by support on request

SlowHttp

Field Type Notes
enabled boolean default true; Apply the timeouts below and wait for the complete request before passing it on. Off: the edge defaults apply, 10 s for the request and 5 s for client-fin
timeout_http_request string default "5s"; may be null; Time a client has to send the complete request, or it gets 408: a number with the unit us, ms, s, m, h or d, such as 5s. Null: 5s
timeout_client_fin string default "5s"; may be null; Time a client that half-closed its connection has to finish it: a number with the unit us, ms, s, m, h or d, such as 5s. Null: 5s

ThreatIntel

Field Type Notes
mode string one of off, log, challenge, deny; default "log"; Hijacked and criminal netblocks and malware command servers (Spamhaus DROP, abuse.ch)
abuse string one of off, log, challenge, deny; default "log"; Sources reported for attacks in the last 48 hours (blocklist.de, CINS). These addresses rotate and sit behind NAT: prefer challenge over deny
tor string one of off, log, challenge, deny; default "log"; Tor exit nodes

Example

Switch posture during an attack, or raise two limits and challenge Tor:

curl -s -X PATCH -H "Authorization: Bearer $ZP_KEY" -H 'Content-Type: application/json' \
  $API/services/www.example.com -d '{"defense_posture": "under_attack"}'
curl -s -X PATCH -H "Authorization: Bearer $ZP_KEY" -H 'Content-Type: application/json' \
  $API/services/www.example.com -d '{"rate_limiting": {"req_per_10s": 1000,
    "path_aggregate_per_10s": 20000}, "threat_intel": {"tor": "challenge"}}'

Behaviour

  • A source is one IPv4 address or one IPv6 /64; per-source counters are shared by all edges. *_per_10s counts per 10 s, cookie_ttl is seconds, the timeouts are durations such as 5s.
  • Postures exist for HTTPS services. standard and under_attack turn on proof-of-work, so they are refused for service_profile api and hls: use off and set the limits directly. Changes after applying a posture show as posture_drift.
  • sensitivity derives the service-wide limits (requests per path, new sessions per second, challenge submissions, origin connections) from your normal peak: the busiest regular day of the last 14, measured nightly, attack days left out. Never below the posture's value, scaled by size_tier (S 0.5×, M 1×, L 2×, XL 4×; needs a sensitivity); under_attack overrides it; per-source limits stay.
  • path_aggregate_per_10s acts only while your origin is under load (80 % of its connection limit in use, or requests waiting). Sources with normal answers in at least five different minutes, not away for more than half an hour, keep access until they send three times the limit themselves. Set it above the real peak of your busiest single path.
  • A fingerprint is shared by everyone running the same software, so ja4_action: deny blocks real users too. It does not catch attacks through real browsers or varying TLS stacks.
  • Proof-of-work is an Argon2 challenge. difficulty 16 to 20: each step doubles the work, 18 is about a thousand Argon2 hashes in the browser. A solved challenge counts only on this vService and at this difficulty, for cookie_ttl seconds; raising the difficulty challenges everyone again. bind_ip ties the solved cookie to the solver's IPv4 /24 or IPv6 /64. Answers are limited to 30 per source per minute and to botcheck_per_10s per service, set by ZERO; both answer 429. While the origin is under load, every unknown visitor asking for a page is challenged first. always_challenge is lockdown. Machine clients cannot solve it: put them in the allowlist with address and path.
  • admission is returning-visitor priority: you switch enabled, ZERO sets the thresholds. auto_mitigation (beta): see Under attack.
  • timeout_http_request: the complete request headers must arrive in time; raise it only for clients on genuinely slow links. timeout_client_fin: a half-closed connection must finish in time.
  • Threat intelligence: abuse (abuse sources), tor (Tor exit nodes) and mode (network reputation lists), each off, log, challenge or deny; log (default) records the hit as ti_hit without acting.

In the portal: the service's Overview (posture, sensitivity, size).