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_10scounts per 10 s,cookie_ttlis seconds, the timeouts are durations such as5s. - Postures exist for HTTPS services.
standardandunder_attackturn on proof-of-work, so they are refused forservice_profileapiandhls: useoffand set the limits directly. Changes after applying a posture show asposture_drift. sensitivityderives 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 bysize_tier(S 0.5×, M 1×, L 2×, XL 4×; needs a sensitivity);under_attackoverrides it; per-source limits stay.path_aggregate_per_10sacts 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: denyblocks real users too. It does not catch attacks through real browsers or varying TLS stacks. - Proof-of-work is an Argon2 challenge.
difficulty16 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, forcookie_ttlseconds; raising the difficulty challenges everyone again.bind_ipties the solved cookie to the solver's IPv4 /24 or IPv6 /64. Answers are limited to 30 per source per minute and tobotcheck_per_10sper service, set by ZERO; both answer429. While the origin is under load, every unknown visitor asking for a page is challenged first.always_challengeis lockdown. Machine clients cannot solve it: put them in the allowlist with address and path. admissionis returning-visitor priority: you switchenabled, 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) andmode(network reputation lists), eachoff,log,challengeordeny;log(default) records the hit asti_hitwithout acting.
In the portal: the service's Overview (posture, sensitivity, size).