Ask
148
@quietmargin ·

agent retry loop burned $612 in six days before my budget alert fired LLM Costs

Solo, $9/mo tool that cleans up messy CSVs. Two weeks ago I added a small agent loop: model calls a validate_rows tool, tool returns the failing rows, model tries again. No cap on attempts, because in testing it always converged in two.

In production it does not always converge. Some files have a column the validator will never accept, so the loop just runs until the request times out at 120s. Best I can tell that is 30-40 model calls with the whole file in context every time.

Damage: $9, $11, $63, $97, $148, $214 across six days. I only found out because the provider's monthly budget notification hit at $500 on day six. I have now capped attempts at 4 and day seven was $70, which is still 8x what it was.

Two things I don't know how to do properly:

  • attribute spend to a user. Right now I have one API key and a monthly total. I cannot tell you which accounts are expensive.
  • stop it in the request path rather than find out later.

What does everyone use here? Is there a boring answer I'm missing?

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @back_of_envelope · 3h ago · 3 replies

    The boring answer is that you keep your own ledger and stop reading the provider dashboard for anything except reconciliation.

    Every model call returns a usage object. Write one row per call before you return the response:

    request_id, user_id, feature, model,
    input_tokens, cached_input_tokens, output_tokens,
    attempt_no, ts
    

    Cost is then a SQL query with a price table you own, not a dashboard. Two useful things fall out immediately: cost per user, and cost per feature. When I did this the first query showed 4% of accounts were 61% of spend, all of them on one code path.

    Second thing: attempt_no in that table is the column that would have caught this on day one. Alert on p99 attempts, not on dollars. Dollars are a lagging indicator of a loop that is already broken.

    Also, if you are re-sending the whole file every attempt, put the static part of the prompt first and use prompt caching. Cached input tokens are dramatically cheaper than fresh ones, and a retry loop is the single best-shaped workload for it because everything except the last message is identical.

    96
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @quietmargin · 3h ago

      The attempt_no alert is the bit I did not think of, thank you. Loop count is knowable in real time and dollars are not.

      On caching: the file content is in the middle of the prompt because I interleave the validator errors. Sounds like I need to restructure so the file is a fixed prefix and errors accumulate at the end.

      32
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @kraut_corner · 3h ago

      Yes, and cache hits are prefix-based, so any change near the top of the prompt invalidates everything after it. People put a timestamp or a request id in the system message and then wonder why the hit rate is zero.

      28
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @kraut_corner · 3h ago · 2 replies

    Your bug is not the spend, it is an unbounded loop with a network call in it. You'd be equally upset if it was a database.

    The control you want is a ceiling enforced synchronously in the request path. Increment a counter keyed by user and day, before the call, and refuse when it's over:

    const spent = await kv.incrbyfloat(`spend:${uid}:${day}`, estCost)
    if (spent > limits[plan]) throw new SpendCapExceeded()
    

    Estimate before, correct after with the real usage numbers. It drifts a bit and that's fine.

    Provider budget alerts are advisory and they lag. I have seen them arrive 14 hours after the money was gone. They exist so you can find out, not so you can prevent.

    61
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @durable_ines · 3h ago

      If you are already on Workers, a Durable Object per user is the natural home for that counter - single-threaded, strongly consistent, no race between two concurrent requests both reading $9.90 and both deciding they have room. KV is eventually consistent and will happily let a parallel burst through.

      24
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @egress_egon · 3h ago

    One thing nobody mentions until it hurts: log the inputs alongside the usage, not just the counts. When you find the expensive 3% of requests you want to be able to look at what they actually were. I keep a hash of the prompt and a truncated first 500 chars. Cost me about 40MB a month and saved a full day of guessing.

    44
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @dartfit_dana · 3h ago

    You set a monthly budget notification and expected it to behave like a circuit breaker. It is a notification. It notifies. That is the whole product.

    21
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @p99_hana · 3h ago · 2 replies

    Simplest fix is to swap the model for the cheap tier. It's something like 20x less per token, so your $612 becomes $30 and you can stop worrying about the loop entirely.

    13
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @back_of_envelope · 3h ago

      We tried exactly this and it got worse, not better. The small model failed the tool schema more often, so it looped more often, so total tokens went up even though price per token went down. Fix the loop first, then price-shop. Otherwise you're just buying a cheaper way to run forever.

      9
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @napgapnora · 3h ago

    "in testing it always converged in two" is going on a t-shirt.

    7
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report