An image-generation request can disappear from the browser while the provider is still working. That makes a daily allowance a small accounting problem, not just a counter on a button.
This note comes from the quota implementation in RetroPrompt, a portrait project I maintain. The implementation uses a Cloudflare Worker and D1. The important distinction is between a confirmed failure and an outcome we cannot yet confirm.
Each attempt has an ID, an account or anonymous-browser identifier, a UTC day, and a status: pending, success, or failed. The remaining allowance is:
remaining = max(0, daily_limit - successful_attempts - pending_attempts)
A pending request reserves capacity before the provider call starts. A confirmed successful result consumes it. A confirmed failure stops counting against it.
Do not implement reservation as a separate SELECT followed by an unconditional INSERT. Two concurrent requests could both see the same remaining slot. In this project's SQLite-backed implementation, the count check and conditional insert are one statement. A simplified version is:
INSERT INTO attempts (id, account_id, utc_day, status)
SELECT ?, ?, ?, 'pending'
WHERE (
SELECT COUNT(*) FROM attempts
WHERE account_id = ? AND utc_day = ?
AND status IN ('pending', 'success')
) < ?;
Check whether the insert actually wrote a row before starting expensive work. This is an example for a single SQLite database, not a claim that an arbitrary distributed database offers the same concurrency guarantees.
The provider returns an asynchronous task ID. The Worker polls that task until it completes, fails, or stops being observable within the request window.
An earlier version threw an exception when polling returned an explicit failed status. The general exception handler then treated it like a timeout and left the attempt pending. The user lost access to a slot even though the provider had already confirmed failure.
The fix is to handle that terminal status explicitly: update the attempt from pending to failed, return a clear rejection message, and recompute the allowance. Keep the update conditional on the previous status so an already finalized attempt is not accidentally rewritten.
A network timeout after submitting the task does not prove that generation failed. Neither does losing the response while retrieving the completed image. Automatically releasing a slot in these cases can let retries start additional paid jobs.
The current conservative behavior keeps uncertain attempts pending until the next UTC allowance window. That protects the small daily budget, but it is a real user-experience limitation: someone may have to wait even when no image reached their browser. It is not a refund system or an exactly-once guarantee.
A stronger future design would persist provider task IDs and reconcile pending attempts asynchronously. Provider-supported idempotency keys could also help, if their documented behavior matches the retry strategy. Those are improvements to investigate, not features this implementation already guarantees.
A useful regression suite should exercise behavior rather than just check the displayed counter:
Fixture-based provider tests can verify these transitions without spending image credits. They do not establish current provider uptime or output quality. Our recent regression work used that separation.
There is another boundary: an anonymous browser cookie is not a verified person. Clearing it or switching browsers can create a new allowance identity. Strong abuse prevention requires a separate design; the daily counter alone does not provide it.
Project context: RetroPrompt. I maintain the linked project. This article was prepared with AI assistance and checked against its implementation; it reports no traffic or performance benchmark.