ASVS 5.0 V15.1.3 is a Level 2 requirement. Your documentation must identify functionality that is time consuming or resource demanding. It must also explain how you prevent a loss of availability from overuse, and how you avoid a situation where building a response takes longer than the consumer's timeout. Suggested defenses include asynchronous processing, queues, and limits on parallel work per user and per application.
Why it matters
Some endpoints are cheap to call and expensive to serve: report exports, image processing, search with wildcards, bulk imports, PDF rendering, password hashing, regular expression heavy validation. An attacker, or an ordinary customer with a big account, can use them to exhaust CPU, memory, connections or workers. The rest of the application then fails along with them.
The documentation matters because the people who write the next feature need to know which paths are dangerous, and the people who run the system need to know which limits protect them.
How to implement it
Start with a list. For each heavy function, record the cost, the trigger, and the control.
| Function | Cost | Control |
|---|---|---|
| CSV export | Memory, DB time | Async job, 2 concurrent per tenant |
| Image resize | CPU | Queue, size cap, 30 s job timeout |
| Report builder | DB time | Cache, 10 requests per hour per user |
| Search wildcard | DB time | Minimum 3 characters, statement timeout |
Then implement the controls the document claims.
Move long work off the request path and answer with a handle:
@app.post("/exports")
def start_export():
job = queue.enqueue(build_export, current_user.id, job_timeout=300)
return {"job_id": job.id, "status_url": f"/exports/{job.id}"}, 202
@app.get("/exports/<job_id>")
def export_status(job_id):
job = queue.fetch_job(job_id)
return {"status": job.get_status(), "url": job.result if job.is_finished else None}
Limit parallel work per user:
import asyncio
from collections import defaultdict
limits = defaultdict(lambda: asyncio.Semaphore(2))
async def run_export(user_id):
async with limits[user_id]:
return await build_export(user_id)
Set timeouts at every layer, with the inner one shorter than the outer so errors are controlled:
proxy_read_timeout 30s;
SET statement_timeout = '5s';
Cap inputs: maximum upload size, maximum page size, maximum number of items in a batch, maximum nesting depth for JSON and GraphQL queries.
Apply rate limiting at the edge and per account, and return 429 with a Retry-After header.
How to test it
- Find the heavy function list in the documentation. Confirm each item has a stated control.
- Pick one and exercise it: send 20 parallel requests as one user. The system should queue, slow or reject some of them, not degrade for everyone.
- Send an oversized input and confirm a clean rejection.
- Measure the response time of the slowest endpoints under load and compare with the client timeout. Anything near the limit should be async.
- Watch a different, unrelated endpoint during the load to confirm it stays responsive.
A simple load test is enough:
seq 1 20 | xargs -P 20 -I{} curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Authorization: Bearer $T" https://example.com/exports -X POST
Common mistakes
- Documenting the risk and never adding the control.
- A limit per IP address only, which fails behind shared networks and does nothing against a distributed client.
- Making a heavy task async but allowing unlimited jobs in the queue.
- Missing timeouts on outbound calls to other services.
- Forgetting the background workers, which can be starved too.
Quick check
Is making everything asynchronous enough?
No. Queues move the load rather than remove it. You also need limits on queue depth, parallelism and job time.
Why mention the consumer's timeout?
If a response takes longer than the client waits, the client retries, which doubles the load. Async handling with a status endpoint avoids that loop.