{"slug": "handling-timeouts-in-ai-image-generation-without-blind-retries", "title": "Handling Timeouts in AI Image Generation Without Blind Retries", "summary": "A developer outlined a state-machine approach for handling timeouts in asynchronous AI image generation APIs, arguing that a lost HTTP response cannot be treated as a confirmed failure because the provider may have already accepted and billed for the task. The design assigns a local request ID before submission, persists its association with the provider's task ID, and uses states such as status_unknown and storage_failed to separate uncertain submissions from confirmed failures. Application credits are reserved before submission and retained through ambiguity, with retries routed to a stored attempt rather than a new provider submission.", "body_md": "A user clicks Generate. Your backend submits a request to an image provider, then the HTTP call times out. The provider may already be creating an image. Submitting again could create a second paid task.\n\nThe code examples are simplified illustrations.\n\nAn asynchronous generation API still has an initial HTTP exchange: submit a request and receive a task ID. That exchange can fail independently of the work it starts.\n\nA documented rejection can establish that the request was not accepted. A timeout or lost response cannot establish the same thing. The request might never have arrived, or the provider might have accepted it before the response was lost.\n\nThere are two costs to distinguish here. Creating another provider task can incur another upstream charge. Deducting the user's application credits twice is a separate accounting error. A local credit reservation does not prevent duplicate provider tasks, and provider deduplication does not make your local ledger idempotent.\n\nThe application needs an identity before it calls the provider.\n\nA local request ID identifies one intended generation. The provider's task ID identifies the work accepted upstream. Persist their association when a submission response arrives.\n\nA request ID alone does not provide idempotency. Reusing it must lead to a stored attempt rather than another submission. The service must also check ownership and whether the request parameters match the original attempt. Reusing an ID for different input should not silently return an unrelated result.\n\nIn our service, the existing-attempt path checks identity and input consistency. Submission uses persisted state and a lease to coordinate competing requests. The client must retain the ID across recovery; generating a fresh ID for every retry defeats local deduplication.\n\nA useful state model separates uncertainty from confirmed failure, and generation from delivery:\n\n```\n// Teaching model: names describe local application states.\ntype AttemptState =\n  | 'initializing'\n  | 'submitting'\n  | 'pending'\n  | 'status_unknown'\n  | 'storage_failed'\n  | 'completed'\n  | 'failed';\n```\n\n`status_unknown` does not require a provider task ID. It can describe a submission whose response was lost before an ID could be recorded. It can also describe an accepted task whose current result cannot be established.\n\n`storage_failed` is different. An image may have been generated successfully while saving or preparing its deliverable failed. That calls for recovering delivery of the existing task, not generating another image.\n\nThe simplified flow is:\n\n``` php\ninitializing -> submitting -> pending -> completed\n                    |           |\n                    |           +-> storage_failed -> completed\n                    |                 (retry delivery)\n                    v\n               status_unknown\n                    |\n                    +-> pending / completed / storage_failed\n                    |   when an identified task can be checked\n                    +-> failed only when failure is established\n\nA confirmed rejection or generation failure can also lead to failed.\nNo provider ID: uncertainty remains; lookup alone cannot resolve it.\n```\n\nReserve application credits before attempting submission. Persist enough information to connect the reservation to the local attempt.\n\nAfter acceptance, retain the reservation while the task is pending. After an ambiguous submission, retain it while the outcome is unknown. Neither a missing task ID nor a failed status query establishes that the generation failed.\n\nOur service marks an ambiguous submission as `status_unknown` with `SUBMISSION_UNKNOWN`. Its settlement path handles terminal states; it does not release that reservation just because the submission response was lost.\n\nFor the normal generation path:\n\nThat policy leaves unresolved work to handle. An operator may eventually need a documented reconciliation, compensation, or expiry policy. Any such decision should record its reason separately from the provider outcome. Returning a user's credits as compensation does not prove the provider never performed the work. This article does not establish a complete policy for those unresolved attempts.\n\n**With a provider task ID**, query that task. If it is still processing, preserve the attempt and reservation. If the status query itself fails, preserve the uncertainty. If generation succeeds, retrieve and save the result before treating it as delivered.\n\nAn upstream success response is not enough to settle as a completed delivery when the image is still unavailable to the user. Our completion path checks the stored artwork and asset availability.\n\n**Without a provider task ID**, this integration cannot perform its normal task-ID lookup. Replaying the same local request returns the existing attempt rather than automatically creating another provider task.\n\nSome providers offer lookup by a caller-supplied reference, or support idempotent submission. Those capabilities must be confirmed for the specific API, including their retention and request-matching rules. A local request ID is not automatically an upstream idempotency key.\n\nIf no recovery mechanism is available, keep the distinction visible: the outcome is unresolved. A separately requested new generation may incur another provider charge. It should not be disguised as a harmless retry of the original task.\n\nSuppose a provider finishes an image, but saving it fails. Repeating generation would discard the opportunity to deliver an already-created result and could incur another upstream charge.\n\nOur service preserves `storage_failed` as a recoverable state. A later query can retry delivery of the same task. The application retains the reservation until it confirms a deliverable result or reaches another explicit settlement decision.\n\nHere is a small decision sketch. It chooses a recovery action; it does not perform I/O or implement billing:\n\n```\ntype Observation =\n  | { kind: 'submission_unknown' }\n  | { kind: 'query_unknown' }\n  | { kind: 'pending' }\n  | { kind: 'storage_failed' }\n  | { kind: 'confirmed_failure' }\n  | { kind: 'delivery_confirmed' };\n\ntype NextAction =\n  | 'preserve_attempt'\n  | 'retry_existing_delivery'\n  | 'release_once'\n  | 'settle_once';\n\nfunction nextAction(observation: Observation): NextAction {\n  switch (observation.kind) {\n    case 'submission_unknown':\n    case 'query_unknown':\n    case 'pending':\n      return 'preserve_attempt';\n    case 'storage_failed':\n      return 'retry_existing_delivery';\n    case 'confirmed_failure':\n      return 'release_once';\n    case 'delivery_confirmed':\n      return 'settle_once';\n  }\n}\n```\n\nThe word `once` is a requirement, not a guarantee provided by this function. The executor needs durable transition checks, concurrency control, and idempotent ledger operations. It also needs recovery if it crashes after storing the image but before recording settlement. An object store and a billing database do not become one atomic transaction because the calls appear next to each other in code.\n\nIn [TatSketch](https://tatsketch.com/), the AI tattoo design tool I work on, two test definitions capture this distinction:\n\nThese describe tests present in the repository, not a fresh test run or a production reliability measurement.\n\nFor your own integration, also check request-ID reuse with changed input, rapid duplicate submissions, concurrent queries, transient query failures, duplicate settlement attempts, and interruption between delivery and settlement. Verify both the provider call count and the ledger outcome.\n\nThe goal is to keep a network failure from silently becoming a new purchase of upstream work. Preserve the attempt, record what is known, and make recovery and settlement explicit. That gives you a way to investigate uncertainty without pretending it has disappeared.\n\nThis article was written with AI assistance.", "url": "https://wpnews.pro/news/handling-timeouts-in-ai-image-generation-without-blind-retries", "canonical_source": "https://dev.to/zhengguge06/handling-timeouts-in-ai-image-generation-without-blind-retries-4dc4", "published_at": "2026-09-17 07:30:14+00:00", "updated_at": "2026-09-17 07:53:48.929412+00:00", "lang": "en", "topics": ["ai-infrastructure", "ai-products", "developer-tools", "mlops"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/handling-timeouts-in-ai-image-generation-without-blind-retries", "markdown": "https://wpnews.pro/news/handling-timeouts-in-ai-image-generation-without-blind-retries.md", "text": "https://wpnews.pro/news/handling-timeouts-in-ai-image-generation-without-blind-retries.txt", "jsonld": "https://wpnews.pro/news/handling-timeouts-in-ai-image-generation-without-blind-retries.jsonld"}}