I’ve been researching a class of attack that I find particularly interesting because it combines something that already exists (slow HTTP attacks) with something nobody had studied properly: the body reader of modern HTTP frameworks.

The result is called Slow JSON Stream, and the TL;DR is this: with 64 simultaneous connections sending 1 byte per second from a single machine, you can leave a PHP/Laravel server unresponsive in under 2 minutes. No special tools. No prior knowledge required. Less than 1 kbps of total bandwidth.

And 90% of the frameworks I tested are vulnerable by default.

How it works

The idea is simple. When an HTTP server receives a request with Content-Type: application/json, the framework has to read the complete body before it can process it. The JSON parser needs the closing } or ] to know the document is complete.

Slow JSON Stream abuses this: we open a connection, send a valid JSON prefix, something like {"items":[{"a":1},, and drip it at 1 byte per second using Transfer-Encoding: chunked. We never send the closing token.

The server waits. And waits. And waits.

Each of those connections occupies a worker thread, a goroutine, or an event loop slot. Open 64 of them simultaneously and the server’s worker pool is exhausted. Legitimate requests pile up in the queue, time out, and start failing.

POST /api/data HTTP/1.1
Host: target.com
Content-Type: application/json
Transfer-Encoding: chunked

1\r\n
{\r\n
1\r\n
"\r\n
... (1 byte per second, forever)

The payload is syntactically valid JSON at all times. The parser can’t reject it because every byte received so far is a legal prefix of a complete JSON document. It’s blocked waiting for the closing token that never arrives.

How is this different from Slowloris?

Slowloris (2009) is the grandfather of slow HTTP attacks. It targets the header phase: it never sends the final \r\n that terminates the headers. But that attack has been mitigated for 15 years. nginx, Caddy, Apache… all have client_header_timeout set to 10-60 seconds by default.

Slow JSON Stream targets the body reader, a layer that virtually no framework protects. The body equivalent of client_header_timeout (client_body_timeout, MinRequestBodyDataRate, ReadTimeout) either doesn’t exist or is set to minutes in the default config of 30 out of the 32 frameworks I tested.

The closest historical predecessor is R.U.D.Y. / Slow POST (2011), but there are key differences:

  1. R.U.D.Y. uses a declared Content-Length. Slow JSON Stream uses Transfer-Encoding: chunked, so there’s no declared size, and the server waits for the chunk terminator 0\r\n\r\n that never arrives.
  2. R.U.D.Y. sends arbitrary bytes. Slow JSON Stream sends a valid JSON prefix, blocking both the HTTP layer and the JSON parser simultaneously.
  3. R.U.D.Y. is detectable by WAFs inspecting the Content-Length. Slow JSON Stream has no declared size and is indistinguishable from a slow mobile client uploading data.

The results (spoiler: not great)

I tested 41 targets: 32 application frameworks and 9 infrastructure components (proxies, WAFs, API gateways). Results:

  • 37 out of 41 (90%) are vulnerable by default
  • Only 4 resist: go-fiber, python-fastapi with streaming, Traefik, and Apache ModSecurity

The worst cases

PHP/Laravel is the most dramatic. With 64 connections, RSS (resident memory) grows at 258 MB/minute. A 512 MB container falls in under 2 minutes. Immediate impact from a single machine.

Python/Flask and Ruby/Rails are worse in a different way: they don’t degrade gradually, they crash. Flask hits a 94% error rate at C=64. Rails hits 100%. The service simply stops responding to everything.

.NET/ASP.NET Core (Kestrel) accumulates 66-120 MB/minute of RSS. The reason: MinRequestBodyDataRate is disabled by default (value null).

The “latently vulnerable” group

Async frameworks (Node.js, Go, Rust, Python async…) are a different story. At C=64 you barely notice anything: goroutines and coroutines are cheap, consuming ~2 KB per blocked connection vs ~1 MB for a thread.

But they have no mechanism to ever close those connections. The goroutines accumulate indefinitely. In production, where pools are typically exhausted at C=100-500, the same 64-connection attack can take the service down.

Framework Tier Evidence
PHP/Laravel 1 258 MB/min RSS, down in <2 min
.NET/Kestrel 1 66-120 MB/min RSS
Python/Flask 3 (crash) 94% error rate at C=64
Ruby/Rails 3 (crash) 100% error rate at C=64
Node.js (all) 2 No body timeout, slots held indefinitely
Go (gin, echo…) 2 No body timeout, goroutines accumulate
Rust (axum, actix) 2 No body timeout, async tasks accumulate
go-fiber RESISTANT 4 KB buffer, kills connections in ~1s

What works as a defense and what doesn’t

A lot of things that look like defenses don’t actually work. The nginx default client_body_timeout 60s doesn’t help: the attacker sends 1 byte every 59 seconds and the timeout resets on each chunk. Body size limits (client_max_body_size, WAF size limits) only kick in after the complete body arrives, and it never will. IP-based rate limiting limits scale but doesn’t stop a single slow connection.

What actually closes the gap:

nginx:

client_body_timeout 10s;
limit_req zone=one burst=10;

Apache httpd:

RequestReadTimeout body=10,MinRate=100

The MinRate=100 B/s immediately kills a 1 B/s connection.

ASP.NET Core / Kestrel (most critical, since MinRequestBodyDataRate defaults to null):

options.Limits.MinRequestBodyDataRate = new MinDataRate(
    bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));

Go:

srv := &http.Server{ReadTimeout: 10 * time.Second}

Node.js >= 18:

server.requestTimeout = 10_000;

Spring Boot:

server.tomcat.connection-timeout: 10000

Why go-fiber and fastapi-streaming resist

The only two application frameworks that resist by default use different mechanisms.

go-fiber uses fasthttp internally, which has a 4 KB read buffer limit. Any body that doesn’t complete within 4 KB is discarded. Attacker connections die in ~1 second. Simple and effective.

python-fastapi with ijson uses a streaming JSON parser: the handler is invoked per token, not at the end of the complete document. It can return a response (or error) without having read the entire body. Also kills connections in ~1 second.

If your framework needs the complete body before doing anything, you’re vulnerable unless you explicitly set a bandwidth timeout.

CVSS: 6.5 to 8.6 HIGH

The score varies by deployment:

  • Microservice / Kubernetes / API gateway: CVSS 8.6 HIGH (Scope: Changed, because taking down one service cascades to everything that depends on it)
  • Standalone deployment: CVSS 7.5 HIGH
  • Authenticated endpoint: CVSS 6.5 MEDIUM

The base vector is the same for all:

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H

No privileges required, no user interaction, network access only. The AC:L (low complexity) is what concerns me most: you don’t need specialized knowledge or custom tools. Chunked transfer encoding is standard HTTP.

The code and testbed

I’ve published everything on GitHub: the attack CLI, all 41 Dockerized servers to reproduce the experiments, and the full paper.

Repo: github.com/cr0hn/slowjson

To try it against your own server:

git clone https://github.com/cr0hn/slowjson
cd slowjson
pip install -e cli/

# Start all test servers
make up

# Attack a specific target (your servers only)
slowjson attack http://localhost:8001/ingest \
    --payload A --bytes-per-tick 1 --tick-interval-ms 1000 \
    --connections 64 --duration 90 \
    --probe-url http://localhost:8001/healthz \
    --docker-container slowjson-python-fastapi \
    --output results/test.json \
    --i-own-this-server

The --i-own-this-server flag is required. Only use this against servers you own.

Conclusion

What strikes me most about this research isn’t the attack itself (conceptually it’s simple) but that it went unstudied for years. The HTTP framework body reader is a blind spot. The community hardened client_header_timeout in 2009 and forgot about the body.

The fix in most cases is one line of configuration. But you need to know the problem exists first.

If you’re using any of the frameworks on the list and haven’t explicitly configured a bandwidth timeout for request bodies, you’re probably vulnerable.

The full paper is in the repo if you want all the methodological details, result tables, and statistical analysis: paper/paper.pdf.