scrapewright

Never bill in a finally block

6 October 2026 · Fedor Dyatlov

The expensive operation in my scraper is one call to a language model. It happens once per website: the model reads a page, writes a set of CSS selectors, and every page after that is parsed with those selectors for nothing. So that one call is what customers pay for, and it is priced accordingly.

Here is roughly what I wrote to count it:

try:
    recipe = self.model.synthesize(html, url)
finally:
    meter.syntheses += 1

It looks careful. finally is what you reach for when something must not be skipped, and I remember thinking the counter must not be skipped. It is the wrong instinct in billing code and it cost a customer twice.

What happened

Two extractions failed. Both raised, both incremented the counter on the way out, and the customer was charged six hundred credits for two calls that produced nothing.

Nobody got an answer and the bill went up. I had not been billed by my own provider for either call — a failed request is not a charged request on their side either.

The fix is smaller than the bug

recipe = self.model.synthesize(html, url)
meter.syntheses += 1

Count it after it returns. If it raises, the line is never reached, which is the correct behaviour and also the obvious one. The try was not protecting anything; it was protecting the counter from the truth.

The rule underneath it

Cleanup goes in finally. Revenue does not.

finally means "whatever happened, do this". That is exactly right for closing a file, releasing a lock, shutting down a browser. It is exactly wrong for charging money, because what happened is precisely the question that decides whether you may charge.

Once stated that way it generalises. Every metered thing in the service now follows the same shape:

That last one is not generosity, it is arithmetic. The renders cost cents. A customer discovering they were billed for a crawl that returned nothing costs the account.

Why it was easy to miss

Because the failure mode is invisible from the inside. The counter went up, the ledger balanced, every total was internally consistent. Nothing in any log said "this one produced no recipe". The only person who could see it was the customer, looking at a balance that had dropped further than their results explained — and the customer was me, which is the only reason it was ever found.

If you bill for anything, the test worth writing is not "does the meter count". It is: make the expensive thing throw, and assert the balance did not move. That test now exists for every billable unit in the service, and it is the one I would write first in anything I build next.