{"course":{"slug":"context-engineering","title":"Context Engineering","url":"https://addaly.com/learn/context-engineering","level":"advanced","track":"build"},"lesson":{"slug":"denial-of-wallet-and-context-bombs","title":"How one user spends your whole budget","url":"https://addaly.com/learn/context-engineering/denial-of-wallet-and-context-bombs","position":48,"of":87,"minutes":9,"answer":"Cost is requests times tokens times price, and a user can inflate all three without any exploit, so cap input at every entry point, cap output on every call.","keyIdea":"Cost is requests times tokens times price, and a user can inflate all three without any exploit, so cap input at every entry point, cap output on every call, bound agent loops and tool results in code, and enforce a per-user daily token budget counted from billed usage rather than a request rate limit."},"body":{"format":"markdown","text":"## The attack that needs no exploit\n\nMost systems that charge per token have no cap on how many tokens one user can cause you to spend. That is not an unusual oversight; it is the default state of an application built by adding a model call to an existing feature.\n\nThe damage does not require malice. Every one of these has happened by accident:\n\n- A user uploads a 400-page PDF and asks eleven questions about it.\n- An integration retries a failing request every thirty seconds for a weekend.\n- An agent loop with no stopping rule calls a search tool 900 times.\n- A tool returns the entire table because a filter was empty, and the result goes into the window.\n- A document contains 300,000 repeated characters, and the chunker faithfully produces 800 chunks from it.\n\nAnd deliberately, it is easier still: submit a long input, ask for a long output, repeat. There is no vulnerability to find. The feature works as designed.\n\n## The three multipliers\n\nCost is `requests × tokens_per_request × price`. An attacker or an accident can inflate all three, and defences have to cover each.\n\n**Requests.** Rate limiting per user and per key. Ordinary, well-understood, and frequently applied to the HTTP endpoint while the internal agent loop that makes forty model calls per request goes uncounted.\n\n**Input tokens.** Caps at every entry point. Maximum upload size, maximum extracted characters per document, maximum pages, maximum tool result size, maximum history retained. Each cap belongs at the boundary where the data arrives, not in the assembler at the end: by then you have already parsed and embedded it.\n\n**Output tokens.** A hard `max_tokens` on every call. Also a defence against a prompt-injection payload that asks the model to produce as much text as possible, which is a cheap way to run up a bill through a document you did not write.\n\n## The budget that actually works\n\nRate limits do not bound cost, because one request can be a thousand times more expensive than another. Bound the thing you are billed for:\n\n```python\nspent = redis.incrby(f\"tok:{user_id}:{today}\", tokens_used)\nif spent > DAILY_TOKEN_BUDGET[user.tier]:\n    raise BudgetExceeded(user_id, spent)\n```\n\nCount *after* each call using the billed usage figures, and check *before* the next one. Set the budget per tier from the actual distribution of legitimate use (the 99th percentile of your real users, with headroom) rather than from a guess. And define what happens on exhaustion: a clear message, a degraded free path, or a queue, but not a silent failure.\n\nThe same counter, aggregated, gives you a global daily ceiling and a kill switch. A ceiling that stops the whole feature is unpleasant. It is much less unpleasant than a bill that arrives after the weekend.\n\n## Bounding the loops\n\nAgents multiply everything, because one user action becomes an unknown number of calls. Three caps, all enforced in code rather than in instructions:\n\n1. **Maximum iterations** per task. A hard integer.\n2. **Maximum accumulated tokens** per task, checked between steps and abandoning the task when exceeded.\n3. **Maximum tool result size**, truncated at the tool boundary with a note saying it was truncated.\n\nThe third is the one that prevents the specific failure from module one, where an oversized tool result pushed the instructions out of the window. It is a cost defence and a correctness defence at the same time.\n\n## Watch the tail, not the mean\n\nThe metric that catches this early is the 99th percentile of tokens per request, and the maximum, per user and per endpoint. A mean of 6,000 tokens with a maximum of 900,000 is a system with an unbounded path in it, and the mean will keep looking healthy right up until the day somebody finds that path.\n\nAlert on the maximum, not the average. Then find the request behind it and read what was in the window.\n\n## The uncomfortable trade\n\nEvery cap here degrades some legitimate use. A researcher wants to ask eleven questions about a 400-page document. A caps-everywhere system tells them no.\n\nThe honest resolution is tiering rather than pretending the trade does not exist: generous limits for accounts you can bill or verify, tight limits for anonymous and free use, and an explicit path to request more. State the limits in the product rather than failing mysteriously at the boundary: a user who knows there is a 50-page limit uploads the right chapter; a user who does not is a support ticket.\n\nWhat you should not do is leave it unbounded because bounding it is awkward. The awkwardness is finite and the bill is not."},"check":{"question":"A team adds a strict per-user request rate limit, yet a single account still produces a very large bill. What did the rate limit fail to bound?","options":[{"text":"The number of retries, which bypass rate limiting because they reuse the original request id.","correct":false,"tempting":"Retries are usually counted like any request; and the limit applies per user regardless of id reuse."},{"text":"The tokens per request, since one request can carry a huge document and trigger many internal model calls.","correct":true},{"text":"The price per token, which rises with usage under most providers' pricing.","correct":false,"tempting":"Inverts how volume pricing works; rates fall or stay flat with usage rather than rising."},{"text":"The cache hit rate, which collapses under heavy use from a single account.","correct":false,"tempting":"A single account's repeated traffic would tend to improve cache hits, not destroy them."}]},"prev":{"slug":"routing-by-context-size","title":"Not every request needs the big model","url":"https://addaly.com/learn/context-engineering/routing-by-context-size"},"next":{"slug":"a-conversation-is-a-growing-prefix","title":"Every turn carries all the previous ones","url":"https://addaly.com/learn/context-engineering/a-conversation-is-a-growing-prefix"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'How one user spends your whole budget', in Context Engineering. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/context-engineering/denial-of-wallet-and-context-bombs (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"How one user spends your whole budget","courseTitle":"Context Engineering","module":{"index":5,"slug":"cost-cache-and-latency","title":"Cost, caching and latency"},"answer":null,"keyIdea":"Cost is requests times tokens times price, and a user can inflate all three without any exploit, so cap input at every entry point, cap output on every call, bound agent loops and tool results in code, and enforce a per-user daily token budget counted from billed usage rather than a request rate limit.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"the-attack-that-needs-no-exploit","runs":[{"text":"The attack that needs no exploit"}]},{"t":"p","runs":[{"text":"Most systems that charge per token have no cap on how many tokens one user can cause you to spend. That is not an unusual oversight; it is the default state of an application built by adding a model call to an existing feature."}]},{"t":"p","runs":[{"text":"The damage does not require malice. Every one of these has happened by accident:"}]},{"t":"ul","items":[[{"text":"A user uploads a 400-page PDF and asks eleven questions about it."}],[{"text":"An integration retries a failing request every thirty seconds for a weekend."}],[{"text":"An agent loop with no stopping rule calls a search tool 900 times."}],[{"text":"A tool returns the entire table because a filter was empty, and the result goes into the window."}],[{"text":"A document contains 300,000 repeated characters, and the chunker faithfully produces 800 chunks from it."}]]},{"t":"p","runs":[{"text":"And deliberately, it is easier still: submit a long input, ask for a long output, repeat. There is no vulnerability to find. The feature works as designed."}]},{"t":"h2","id":"the-three-multipliers","runs":[{"text":"The three multipliers"}]},{"t":"p","runs":[{"text":"Cost is "},{"text":"requests × tokens_per_request × price","code":true},{"text":". An attacker or an accident can inflate all three, and defences have to cover each."}]},{"t":"p","runs":[{"text":"Requests.","b":true},{"text":" Rate limiting per user and per key. Ordinary, well-understood, and frequently applied to the HTTP endpoint while the internal agent loop that makes forty model calls per request goes uncounted."}]},{"t":"p","runs":[{"text":"Input tokens.","b":true},{"text":" Caps at every entry point. Maximum upload size, maximum extracted characters per document, maximum pages, maximum tool result size, maximum history retained. Each cap belongs at the boundary where the data arrives, not in the assembler at the end: by then you have already parsed and embedded it."}]},{"t":"p","runs":[{"text":"Output tokens.","b":true},{"text":" A hard "},{"text":"max_tokens","code":true},{"text":" on every call. Also a defence against a prompt-injection payload that asks the model to produce as much text as possible, which is a cheap way to run up a bill through a document you did not write."}]},{"t":"h2","id":"the-budget-that-actually-works","runs":[{"text":"The budget that actually works"}]},{"t":"p","runs":[{"text":"Rate limits do not bound cost, because one request can be a thousand times more expensive than another. Bound the thing you are billed for:"}]},{"t":"code","lang":"python","label":"Python","text":"spent = redis.incrby(f\"tok:{user_id}:{today}\", tokens_used)\nif spent > DAILY_TOKEN_BUDGET[user.tier]:\n    raise BudgetExceeded(user_id, spent)"},{"t":"p","runs":[{"text":"Count "},{"text":"after","i":true},{"text":" each call using the billed usage figures, and check "},{"text":"before","i":true},{"text":" the next one. Set the budget per tier from the actual distribution of legitimate use (the 99th percentile of your real users, with headroom) rather than from a guess. And define what happens on exhaustion: a clear message, a degraded free path, or a queue, but not a silent failure."}]},{"t":"p","runs":[{"text":"The same counter, aggregated, gives you a global daily ceiling and a kill switch. A ceiling that stops the whole feature is unpleasant. It is much less unpleasant than a bill that arrives after the weekend."}]},{"t":"h2","id":"bounding-the-loops","runs":[{"text":"Bounding the loops"}]},{"t":"p","runs":[{"text":"Agents multiply everything, because one user action becomes an unknown number of calls. Three caps, all enforced in code rather than in instructions:"}]},{"t":"ol","start":1,"items":[[{"text":"Maximum iterations","b":true},{"text":" per task. A hard integer."}],[{"text":"Maximum accumulated tokens","b":true},{"text":" per task, checked between steps and abandoning the task when exceeded."}],[{"text":"Maximum tool result size","b":true},{"text":", truncated at the tool boundary with a note saying it was truncated."}]]},{"t":"p","runs":[{"text":"The third is the one that prevents the specific failure from module one, where an oversized tool result pushed the instructions out of the window. It is a cost defence and a correctness defence at the same time."}]},{"t":"h2","id":"watch-the-tail-not-the-mean","runs":[{"text":"Watch the tail, not the mean"}]},{"t":"p","runs":[{"text":"The metric that catches this early is the 99th percentile of tokens per request, and the maximum, per user and per endpoint. A mean of 6,000 tokens with a maximum of 900,000 is a system with an unbounded path in it, and the mean will keep looking healthy right up until the day somebody finds that path."}]},{"t":"p","runs":[{"text":"Alert on the maximum, not the average. Then find the request behind it and read what was in the window."}]},{"t":"h2","id":"the-uncomfortable-trade","runs":[{"text":"The uncomfortable trade"}]},{"t":"p","runs":[{"text":"Every cap here degrades some legitimate use. A researcher wants to ask eleven questions about a 400-page document. A caps-everywhere system tells them no."}]},{"t":"p","runs":[{"text":"The honest resolution is tiering rather than pretending the trade does not exist: generous limits for accounts you can bill or verify, tight limits for anonymous and free use, and an explicit path to request more. State the limits in the product rather than failing mysteriously at the boundary: a user who knows there is a 50-page limit uploads the right chapter; a user who does not is a support ticket."}]},{"t":"p","runs":[{"text":"What you should not do is leave it unbounded because bounding it is awkward. The awkwardness is finite and the bill is not."}]}],"check":{"question":[{"text":"A team adds a strict per-user request rate limit, yet a single account still produces a very large bill. What did the rate limit fail to bound?"}],"options":[{"text":[{"text":"The number of retries, which bypass rate limiting because they reuse the original request id."}],"correct":false,"tempting":[{"text":"Retries are usually counted like any request; and the limit applies per user regardless of id reuse."}]},{"text":[{"text":"The tokens per request, since one request can carry a huge document and trigger many internal model calls."}],"correct":true,"tempting":null},{"text":[{"text":"The price per token, which rises with usage under most providers’ pricing."}],"correct":false,"tempting":[{"text":"Inverts how volume pricing works; rates fall or stay flat with usage rather than rising."}]},{"text":[{"text":"The cache hit rate, which collapses under heavy use from a single account."}],"correct":false,"tempting":[{"text":"A single account’s repeated traffic would tend to improve cache hits, not destroy them."}]}],"order":[2,3,0,1],"why":null},"prev":{"slug":"routing-by-context-size","title":"Not every request needs the big model","minutes":8},"next":{"slug":"a-conversation-is-a-growing-prefix","title":"Every turn carries all the previous ones","minutes":8}}}