HTTP Performance and Load Behavior
The headline throughput number is in the Comparison table
(CI-measured, release build, firewall limits raised for the test). This page
describes how 3va behaves at its shipped default limits, which is a
deliberate design choice, not a performance regression — a single-machine
load test against defaults will see most requests rejected with 403/429,
and that’s intentional. vvva_firewall operates in the connection-accept
loop, before any byte reaches JavaScript, so the runtime never processes a
request that exceeds a safety limit.
Default limits
| Field | Default | Purpose |
|---|---|---|
rateLimitRps | 100 req/s per IP | token-bucket sustained rate |
rateLimitBurst | 200 | burst capacity before throttling kicks in |
maxConnectionsPerIp | 50 | simultaneous connections per IP |
maxConnectionsTotal | 10,000 | simultaneous connections, all IPs |
headerTimeoutMs | 10,000 | deadline to receive full headers (Slowloris) |
bodyTimeoutMs | 30,000 | deadline to receive the full body |
minBodyRateBps | 100 B/s | minimum sustained body receive rate (RUDY) |
maxHeaderCount | 100 | max headers per request |
maxHeaderBytes | 16 KiB | max combined header size |
autoBlockThreshold | 10 violations | violations before auto-block |
blockDurationSecs | 300 s | base auto-block duration |
blockEscalationFactor | 2 | multiplier per repeat offense |
maxBlockDurationSecs | 3,600 s | cap on escalated block duration |
strikeDecaySecs | 3,600 s | calm period before strike history resets |
At high concurrency, the connection limiter deliberately sheds excess connections rather than queuing them indefinitely, trading raw throughput for protection against overload.
Slowloris and RUDY protection
- Slowloris (headers trickled in one line per second) —
headerTimeoutMsgives everyread_linean independent deadline. - RUDY (R-U-Dead-Yet — an extremely slow POST body) —
bodyTimeoutMsis the total body-read deadline, andminBodyRateBpsis a minimum average receive rate: a body arriving below the floor is aborted after ~2s of grace, without waiting for the full 30s deadline.
Adaptive auto-block escalation
autoBlockThreshold + blockDurationSecs are the base layer: repeated
violations trigger an automatic block of fixed duration. On top of that, the
firewall is adaptive — each auto-block records a strike per IP, and the
block duration escalates with reincidence:
auto-block #n → duration = min(blockDurationSecs × blockEscalationFactor^(n-1), maxBlockDurationSecs)With defaults (base 300s, factor ×2, cap 3600s): 1st offense → 300s, 2nd →
600s, 3rd → 1200s, 4th → 2400s, 5th → 3600s (capped). Strike history is
not cleared when a block expires — a repeat attacker gets the next tier.
It’s only cleared once the IP goes strikeDecaySecs without another
auto-block. This escalation penalizes the block duration for repeat
offenders — it does not touch the per-IP token bucket rate itself.
Adaptive rate limiting (EWMA baseline)
A separate, opt-in mechanism (adaptiveRateLimit: true) solves the inverse
problem: legitimate traffic spikes. With a fixed threshold, an IP whose
legitimate traffic grows gradually (a heavy client, a corporate proxy)
crosses rateLimitRps and accumulates violations even though nothing
malicious is happening.
window: 1s
baseline: ewma = α × count_window + (1 − α) × ewma_prev (α = ewmaAlphaPct / 100, default 20 → α = 0.20)
effective rps: max(rateLimitRps, ceil(ewma × 1.5)), capped at rateLimitRps × 4.0The token bucket consumes against the effective rate, so a high-baseline IP
gets more tokens/second without its burst changing. If the IP returns to low
traffic, the EWMA decays back down over subsequent windows. An attacker
starting from zero has no observed history, so ewma = 0 and the threshold
stays static. Disabled by default.
Trusted reverse proxies
trustedProxies (empty by default) lists proxy IPs/CIDRs allowed to supply
a client IP via X-Forwarded-For. Connection tracking is per-source-IP —
behind an untrusted or unconfigured reverse proxy that collapses many
clients onto one IP, the proxy’s own IP is what gets limited.
Request smuggling guard
The request parser supports Transfer-Encoding: chunked and rejects any
request carrying both Content-Length and Transfer-Encoding with 400.
Configuring limits
export default {
firewall: {
enabled: true,
rateLimitRps: 100,
rateLimitBurst: 200,
trustedProxies: [],
autoBlockThreshold: 10,
blockDurationSecs: 300,
blockEscalationFactor: 2,
maxBlockDurationSecs: 3600,
strikeDecaySecs: 3600,
maxConnectionsPerIp: 50,
maxConnectionsTotal: 10_000,
headerTimeoutMs: 10_000,
bodyTimeoutMs: 30_000,
minBodyRateBps: 100,
maxHeaderCount: 100,
maxHeaderBytes: 16_384
}
}