A video-generation request times out. There is no task ID in the response. Should the application send it again?
The awkward answer is that the provider might already be generating the video. The response could have disappeared after the task was accepted. Sending the same payload again could start another paid task.
I built I Love the Smiths AI, a small photo-to-video app that turns one or two photos into a 20-second elevator scene. The user-facing flow is short. Behind it, I need to distinguish between a failed generation and a request whose outcome I simply do not know.
This is how the job-processing code handles that distinction.
Two kinds of duplicate request
The first duplicate comes from the browser. A user clicks again, refreshes, or loses the response from my server.
Each generation carries a request ID. The server uses that ID as the job's primary key. When it receives an existing ID, it checks that the owner and serialized input match the original request. A matching request returns the existing job; a changed input or different owner is rejected.
That check matters. Reusing an ID cannot quietly turn a request for one person's photos into a request for someone else's.
Job creation also debits the user's video allowance through a database trigger. The insertion and debit belong to the same database operation. If the allowance cannot be spent, the job insertion rolls back. A duplicate insertion does not spend another allowance.
The second duplicate is harder: submitting the job to the external video service twice. My database can recognize a repeated browser request. That alone cannot deduplicate work inside another system.
Record the submission attempt before making the call
Before calling the provider, the worker persists a submission timestamp. That update is conditional: the job must still be queued, the worker must hold the current claim, and there must be no earlier submission marker.
If the call returns a task ID, the worker saves it and moves the job into processing. Later attempts can ask about that specific task instead of creating another one.
If the response is lost, the record is different: the submission marker exists, but the provider task ID is missing. The code leaves the allowance reserved and exposes a support-review message. It does not automatically submit again.
These are the recovery decisions, reduced to a table:
| What the application knows | Next action |
|---|---|
| No submission marker or provider task ID | Claim the job, record the attempt, then submit |
| A provider task ID exists | Check that task's status |
| A submission marker exists without a task ID | Hold for reconciliation; do not create another task |
| The provider definitely rejected the request | Record failure and restore the allowance |
There is a cost to recording the marker first. A worker could stop after writing it but before sending the request. That job would also need review, even though no provider task was created.
I accept that uncertainty in this integration. It avoids automatically turning an unknown result into another billable submission. The marker is a guard against automatic resubmission, not proof that the provider accepted anything.
A lease needs a claim number
More than one execution can reach a pending job. A scheduled reconciliation pass may overlap another execution, and a slow network operation can outlive the worker's lease.
The worker therefore claims a job through a conditional database update. Each successful claim increments an attempt counter. Subsequent writes must match that counter and an unexpired lease.
The condition has this shape, with names shortened for clarity:
UPDATE video_job
SET status = ?, updated_at = ?
WHERE id = ?
AND attempts = ?
AND lease_until > ?
AND status IN ('queued', 'processing', 'composing', 'saving')
RETURNING *;
Suppose worker A has claim 4 and waits on a response. Its lease expires. Worker B takes over and gets claim 5. When A eventually receives its response, its claim-4 update cannot overwrite B's work.
Checking only a timestamp would be insufficient: A might observe that the job now has a valid lease, but that lease belongs to B. The claim number identifies which execution owns the write.
This protects local state. It does not cancel an external request that is already in flight, which is why the submission marker is a separate concern.
Saving the file is another recoverable step
The provider finishing a video is not the end of the job. The application still needs to fetch, validate, and store the result.
The code records the provider's result URL before starting that copy. On a later processing attempt, an existing result URL sends the job back into the saving step. A storage failure therefore does not require another model-generation request.
Output object keys also include the claim number. An old worker finishing an upload late cannot overwrite the object uploaded by a newer claim. Publishing the saved file as the job's result still requires the guarded database update.
That separation is useful because a network failure while copying bytes says nothing about whether the model generated the scene successfully.
Failure and uncertainty have different accounting
A confirmed provider failure, a definite submission rejection, or an invalid generated video can move the job to failed. A database trigger restores the video allowance using a unique refund reference, so the transition cannot repeatedly credit the same job.
An ambiguous submission does not take that path. Its allowance stays reserved while the outcome is unresolved. Here, restoring an allowance means returning a generation use to the order; it is separate from issuing a payment refund.
The regression cases in the repository include concurrent duplicate requests, an unknown submission that must not be recreated, stale worker responses, and a storage failure that resumes from the recorded result. Those cases describe the boundaries I want the implementation to preserve.
The remaining compromise is manual reconciliation when submission acknowledgement is lost. A provider-supported idempotency key or reliable lookup by client request ID could improve that recovery path. Until the integration has that guarantee, I prefer to make the uncertain state explicit.
The product demo shows the short scene this workflow supports. The same distinction between rejection, completion, and uncertainty is worth making anywhere a retry can start another paid job.
Top comments (0)