Safeguard
Application Security•

ASVS 5.0 V15.1.3: Document Resource Heavy Functionality

ASVS 5.0 V15.1.3 (Level 2): list the slow or expensive features and how you stop them from taking the app down. Queues, limits and timeouts with code.

Safeguard Security Team
4 min read

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.

FunctionCostControl
CSV exportMemory, DB timeAsync job, 2 concurrent per tenant
Image resizeCPUQueue, size cap, 30 s job timeout
Report builderDB timeCache, 10 requests per hour per user
Search wildcardDB timeMinimum 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

  1. Find the heavy function list in the documentation. Confirm each item has a stated control.
  2. 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.
  3. Send an oversized input and confirm a clean rejection.
  4. Measure the response time of the slowest endpoints under load and compare with the client timeout. Anything near the limit should be async.
  5. 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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.