Retry only network failures, HTTP 429, and server failures. Keep the same bytes for every attempt.
imgd.dev uses the SHA-256 content hash, so identical bytes return the same hash and public URL.
Stop on 401, 402, 413, and 415 until the caller changes something.
Every imgd.dev image is public. Do not upload private, confidential, or secret material.
imgd.dev charges a one-time $1 per GB storage price. Purchases stack and do not expire. There is no free tier, so fund the account before the first upload.
Why an uncertain result is safe
A connection can fail after the server accepts the bytes. The caller then does not know whether the upload succeeded.
A random object name can create two stored objects after a retry. A content hash avoids that result. imgd.dev uses the image bytes as the identity:
same bytes -> same SHA-256 hash -> same public URL
A repeated upload can return HTTP 200 with "deduplicated": true.
A new upload normally returns HTTP 202 with "status": "processing".
Both responses identify the same bytes with the same hash and URL.
Use the bounded Node.js example
The complete example is in examples/engineering/retry-upload.mjs.
It uses Node.js built-ins and makes at most four attempts for one upload operation.
It then uploads the same bytes again and compares both results.
Set the key outside the repository:
# IMGD_KEY must already exist in this shell.
node examples/engineering/retry-upload.mjs ./screenshot.png
The script never prints $IMGD_KEY. It sends the key only in the Authorization header.
Do not put a key in a URL, source file, command history, or log.
The retry policy is small and explicit:
const MAX_ATTEMPTS = 4;
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
const request = makeRequest();
let response;
try {
response = await fetch("https://imgd.dev/v1/upload", request);
} catch (networkError) {
if (attempt === MAX_ATTEMPTS) throw networkError;
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
continue;
}
const parsed = await response.json().catch(() => ({}));
const body = parsed && typeof parsed === "object" ? parsed : {};
if (response.ok) return body;
const message = body.fix ?? `upload failed with HTTP ${response.status}`;
if (!RETRYABLE_STATUS.has(response.status) || attempt === MAX_ATTEMPTS) {
throw new Error(message);
}
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
}
The maintained example also rebuilds FormData for each attempt.
It computes the local SHA-256 hash and checks the public URL response.
Expected result
A successful run prints JSON like this:
{
"hash": "<64-character-sha256>",
"url": "https://i.imgd.dev/i/<64-character-sha256>",
"first_http_status": 202,
"repeated_http_status": 200,
"repeated_deduplicated": true,
"same_hash": true,
"same_url": true,
"public_url_http_status": 200
}
The second response can have another success status when concurrent work exists.
The required checks are same_hash, same_url, and a successful public URL request.
The public URL works while moderation has processing status.
It serves a strong blurred placeholder until the check allows recognizable bytes.
Branch on the status before a retry
| Status | Action |
|---|---|
| Network error | Retry the same bytes within the attempt limit. |
429 |
Wait, honor Retry-After when present, and retry within the limit. |
500, 502, 503, 504 |
Use backoff and retry within the limit. |
401 |
Stop. Fix the missing or invalid key. |
402 |
Stop. Buy storage or delete stored images, then start a new attempt. |
413 |
Stop. Resize the image below the current limit, then start again. |
415 |
Stop. Convert the file or correct its media type, then start again. |
Other 4xx |
Stop. Read error and show fix without a blind retry. |
Do not retry every 4xx response. Most client errors require a changed request or account state.
See how actionable API errors help agents recover for the full branch model.
Verify the stored result
The example performs three checks:
- It computes SHA-256 from the local bytes.
- It checks both API hashes against that value.
- It fetches the returned public URL and requires an image response.
You can also inspect the image record:
curl "https://imgd.dev/v1/images/$HASH" \
-H "Authorization: Bearer $IMGD_KEY"
A terminal status is live, review, or blocked.
A review state is terminal and does not clear through more polls.
Recovery rules
- Preserve the original bytes until the operation finishes.
- Do not recompress or rewrite the file between attempts.
- Keep the retry count bounded.
- Show the server
fixvalue exactly when the request needs a caller change. - Record the returned hash and URL, but never record
$IMGD_KEY. - Start a new operation only after a required caller change.
If an expiry matters, send it on every upload of the same bytes. A repeated upload replaces the prior expiry choice. Read how temporary URLs keep the same link before you combine retries with expiry.
When this design fits
Use this design for CI screenshots, agent output, documentation images, and other public images. It fits callers that can retain the same byte array across a short retry window.
Do not use imgd.dev for private images, video, a free trial, or full digital-asset management. Use another service when you need access control or a broad media workflow.
For a basic Node.js upload, read upload an image with JavaScript and Node.js.
For an agent-led setup, use /integrate.md.
For a human setup, open Get started.