[Feature Request] Per-upstream concurrency limit to avoid HTTP 429 from DoH servers
Summary
Please consider adding a per-server option that caps the number of in-flight requests SmartDNS keeps open against a single upstream, e.g.:
server-https https://doh.pub/dns-query -group Direct -max-concurrent-queries 16Some public DoH providers reject excess concurrent requests on a shared HTTP/2
connection with HTTP 429. Because SmartDNS multiplexes all queries for one upstream
over a single connection and has no way to bound that concurrency, bursts of parallel
queries reliably trigger 429 even when the average query rate is very low.
Environment
| Item | Value |
|---|---|
| SmartDNS version | 1.2026.08.05-0921 (Release48.4) |
| OS | OpenWrt / iStoreOS 24.10.8, x86_64 |
| Affected upstream | server-https https://doh.pub/dns-query -group Direct (Tencent DNSPod) |
Observed behaviour
Over a 19-hour window, one router logged 557 occurrences of:
[WARN][client_http2.c:709] http2 server query from 1.12.12.12:443 failed, server return http code: 429The important detail is the time distribution. Grouping the 429s by second:
1 per second -> 64 occurrences
16 per second -> 3 occurrences
33 per second -> 2 occurrences
38 per second -> 1 occurrenceand the largest burst has all 38 rejections inside a 4 ms window
(09:32:03.440 → 09:32:03.444).
Meanwhile, per-upstream statistics for the same period show this server received only 402 queries in 35 minutes (~0.19 qps). So this is not a sustained-rate limit being exceeded — it is momentary in-flight concurrency on the single multiplexed connection.
Note that the server answers HTTP 429 rather than RST_STREAM(REFUSED_STREAM), so
this appears to be an application-level concurrency policy rather than HTTP/2
SETTINGS_MAX_CONCURRENT_STREAMS enforcement.
Controlled reproduction
Issuing N concurrent DoH requests multiplexed over one HTTP/2 connection
(curl --http2 --parallel, deliberately without --parallel-immediate so curl
reuses a single connection):
| concurrent streams | doh.pub |
doh.360.cn |
223.5.5.5 |
|---|---|---|---|
| 10 | 200 × 10 | 200 × 10 | 200 × 10 |
| 40 | 200 × 21, 429 × 19 | 200 × 40 | 200 × 40 |
| 80 | 200 × 11, 429 × 69 | 200 × 80 | 200 × 80 |
doh.pub starts rejecting somewhere between 10 and 40 concurrent streams; the other two
providers tested handle 80 without a single 429. A cap of roughly 16 would keep
SmartDNS comfortably under this particular provider's threshold.
Why the existing options do not cover this
- There is no per-server option for rate or concurrency. The complete set accepted by
_config_serveris-group,-exclude-default-group,-blacklist-ip,-whitelist-ip,-check-edns,-set-mark,-subnet,-subnet-all-query-types,-host-ip,-host-name,-http-host,-interface,-fallback,-spki-pin,-tls-host-verify,-no-check-certificate,-bootstrap-dns,-tcp-keepalive,-drop-packet-latency,-hitchhiking— none of which bounds concurrency. max-query-limitis a single global setting, not per-server. Lowering it to protect one upstream would throttle every upstream and every client equally.-fallbackdoes reduce traffic to the server, but it changes the semantics to standby-only. It is a reasonable workaround, not a way to keep an upstream in active rotation at a safe concurrency.- Switching the same provider to DoT (
server-tls tls://dot.pub) was measured on the same hardware and load: 46.2 % success (37/80) versus 100 % for the other two upstreams in the group, so DoT is not a viable substitute here.
Suggested behaviour
When the configured limit is reached for a server, queries beyond it could either be briefly queued or simply skipped for that upstream — other servers in the group would still answer, so no client-visible failure. Applying the limit per server rather than globally keeps it useful for mixed groups where providers have very different tolerances.
Related issues
- #2398 — SIGSEGV/SIGABRT when an upstream DoH returns HTTP 429. That crash is fixed, and Release48.4 no longer crashes under these bursts. A concurrency cap would reduce exposure to the trigger in the first place.
- #2393 — asks about disabling parallel querying across multiple upstreams. Related but different: this request is about bounding in-flight requests to a single upstream while keeping parallel group behaviour.
Source: pymumu/smartdns