{"slug": "a-timeout-is-not-a-failed-job-handling-daily-quotas-for-an-ai-image-generator", "title": "A timeout is not a failed job: handling daily quotas for an AI image generator", "summary": "A developer maintaining RetroPrompt, an AI portrait image-generation project, described a quota-handling design built on a Cloudflare Worker and D1 that distinguishes confirmed failures from unconfirmed outcomes by tracking each attempt as pending, success, or failed. The implementation reserves capacity with a single conditional SQLite insert to avoid race conditions, and keeps uncertain attempts pending until the next UTC window rather than releasing slots that might correspond to paid provider jobs. The developer notes the conservative approach protects the daily budget but can make users wait, and that anonymous browser cookies do not provide real abuse prevention.", "body_md": "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.\n\nThis 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.\n\nEach attempt has an ID, an account or anonymous-browser identifier, a UTC day, and a status: pending, success, or failed. The remaining allowance is:\n\n```\nremaining = max(0, daily_limit - successful_attempts - pending_attempts)\n```\n\nA pending request reserves capacity before the provider call starts. A confirmed successful result consumes it. A confirmed failure stops counting against it.\n\nDo 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:\n\n```\nINSERT INTO attempts (id, account_id, utc_day, status)\nSELECT ?, ?, ?, 'pending'\nWHERE (\n  SELECT COUNT(*) FROM attempts\n  WHERE account_id = ? AND utc_day = ?\n    AND status IN ('pending', 'success')\n) < ?;\n```\n\nCheck 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.\n\nThe provider returns an asynchronous task ID. The Worker polls that task until it completes, fails, or stops being observable within the request window.\n\nAn 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.\n\nThe 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.\n\nA 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.\n\nThe 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.\n\nA 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.\n\nA useful regression suite should exercise behavior rather than just check the displayed counter:\n\nFixture-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.\n\nThere 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.\n\nProject context: [RetroPrompt](https://retroprompt.site/?utm_source=devto&utm_medium=referral&utm_campaign=blog_20260923). I maintain the linked project. This article was prepared with AI assistance and checked against its implementation; it reports no traffic or performance benchmark.", "url": "https://wpnews.pro/news/a-timeout-is-not-a-failed-job-handling-daily-quotas-for-an-ai-image-generator", "canonical_source": "https://dev.to/yulingg_zhang_dfbce7a345a/a-timeout-is-not-a-failed-job-handling-daily-quotas-for-an-ai-image-generator-213c", "published_at": "2026-09-23 06:05:30+00:00", "updated_at": "2026-09-23 06:22:39.229046+00:00", "lang": "en", "topics": ["ai-products", "ai-infrastructure", "developer-tools", "mlops"], "entities": ["RetroPrompt", "Cloudflare Workers", "Cloudflare D1", "SQLite"], "alternates": {"html": "https://wpnews.pro/news/a-timeout-is-not-a-failed-job-handling-daily-quotas-for-an-ai-image-generator", "markdown": "https://wpnews.pro/news/a-timeout-is-not-a-failed-job-handling-daily-quotas-for-an-ai-image-generator.md", "text": "https://wpnews.pro/news/a-timeout-is-not-a-failed-job-handling-daily-quotas-for-an-ai-image-generator.txt", "jsonld": "https://wpnews.pro/news/a-timeout-is-not-a-failed-job-handling-daily-quotas-for-an-ai-image-generator.jsonld"}}