PiAPI Retry Safety: Prevent Duplicate Tasks and Charges

A timeout after submitting a PiAPI task leaves an application with an uncertain outcome: the request may have failed before creation, or the task may already be running. A safe integration separates task creation from status checks, records an idempotency decision in its own database, and reconciles before submitting another paid task. This guide shows a retry pattern for the Unified API without claiming that a transport retry can cancel or duplicate a remote task safely.
1. Persist the request before you retry
Give each user operation a stable application job ID and save the model, request parameters, creation attempt, and current task ID before handing control to a retry worker. Keep the PiAPI task ID separate from your job ID: the task ID identifies a remote task, while the job ID identifies your own business operation. A missing task ID means the outcome is unknown, not that a second submission is safe.
application job
├─ request fingerprint
├─ creation attempts
├─ piapi task_id (nullable)
└─ completion record (unique)A request fingerprint can detect an accidental second submission from the same job, but it is not a provider-side idempotency key. Store the fingerprint with the fields that matter to your product, including the model and task type, and decide how long a request remains eligible for reconciliation before asking a user to retry explicitly.
2. Retry transport failures as reconciliation work
Retry bounded network failures and 5xx responses with increasing delays and jitter. After a create-task timeout, first look for a task ID returned by a completed response or an application record written by the request worker. If no ID is available, use the documented API contract and your own request ledger to decide whether the attempt can be reconciled. Do not interpret a client timeout as a confirmed remote failure.
Once you have a task ID, query that task and branch on data.status. A completed task should be handled exactly once; a failed task should preserve its error; an in-progress task should return to the bounded worker. If the status endpoint is unavailable, keep the job in an unknown or retryable state and make the next check safe to repeat.
create timeout → reconcile saved job → task_id found?
├─ yes → get task → one completion handler
└─ no → keep unknown → retry reconciliation
→ submit again only after policy allows3. Make completion handling idempotent
Use one completion handler for polling and webhooks. Protect the transition with a durable uniqueness rule such as (job_id, completion_action) or (task_id, completion_action), then make notifications and downstream work retry-safe. A webhook can be delivered again, and a poll can observe the same final state repeatedly; both inputs should converge on the same stored completion record. Validate webhook authentication before enqueueing the event, acknowledge quickly after durable acceptance, and keep slow downloads outside the callback request. The task polling and webhooks guide covers the callback shape and bounded polling flow.
A completed task and a saved media object are different facts. Mark generation complete when the task reaches a final state, then run the copy, checksum, and publication steps in a separate retryable workflow. This lets a storage failure retry without creating another generation task. Ignore later events older than the stored final state rather than reopening the job.
4. Separate generation from output storage
Before rollout, test duplicate create responses, delayed webhooks, repeated polls, worker restarts, partial downloads, and an unavailable result URL. Verify the durable job record, notification count, and media object remain consistent after each replay. Log the job ID, task ID when known, attempt number, status transition, and final reconciliation decision without logging API keys or raw customer data. Use the output storage guide for model-specific output parsing and byte-level validation.
References: Unified API Schema, Get Task example, and Webhook documentation. This article describes an application reliability pattern; it is not a promise of provider-side idempotency or a live generation benchmark.
