Long runs and retries
Runs are synchronous. How to handle a 90-second image agent, what to retry, and how to avoid paying twice.
Every run is synchronous today — the connection stays open until the agent finishes. Here is how to build on that without paying twice.
No webhooks, no job ids — yet
There is no callback URL and no polling endpoint. The result comes back on the same connection you opened. Async delivery is on the roadmap; this page is how to work with what exists.
Set your timeout properly
| Agent kind | Typical time |
|---|---|
| Text | 2 to 15 seconds |
| Images | 30 to 90 seconds |
| On-chain | A few seconds, plus network confirmation |
Allow 180 seconds and you will never cut off a run that was about to succeed.
// Node's fetch has no default timeout at all; browsers vary.
// Be explicit either way.
await fetch(url, { signal: AbortSignal.timeout(180_000) });# requests defaults to no timeout - a hung socket hangs forever.
requests.post(url, json=payload, timeout=180)// A serverless function that calls a slow agent must outlive it.
export const maxDuration = 300; // secondsA client timeout does not cancel the run
If you give up at 30 seconds, the agent keeps going, finishes, and the charge stands. You paid for a result nobody read. This is the single most expensive mistake to make against this API — set the timeout high enough the first time.
Put slow work behind your own queue
If a user is waiting on a page, do not make them hold a 90-second request. Accept the job, run it in a worker, and let the front end poll your own status endpoint.
// 1. Your API accepts the job and returns immediately.
POST /api/jobs → { id: "job_123", status: "queued" }
// 2. Your worker calls amnt, with a long timeout, and stores the result.
const run = await amnt.run("studio/logo-mark", { prompt });
await db.jobs.update(id, { status: "done", output: run.output });
// 3. Your front end polls you, not us.
GET /api/jobs/job_123 → { status: "done", output: { images: [...] } }This also puts the retry policy, the rate limiting and the audit trail in one place — your own.
Retries without double charges
- ✅ Safe to retry:
429,503, one500, and a connection error that happened before any bytes came back. - 🚫 Never retry blindly after a client timeout — the first run probably succeeded and was charged.
- 📝 Log
credits_remainingfrom every response. It is the cheapest way to spot a double charge, because the balance moves twice for one job.
const RETRYABLE = new Set([429, 500, 503]);
async function runOnce(agent, input, attempt = 0) {
try {
const res = await fetch(RUN_URL, { ...opts, signal: AbortSignal.timeout(180_000) });
if (RETRYABLE.has(res.status) && attempt < 2) {
const wait = Number(res.headers.get("retry-after") ?? 2 ** attempt);
await sleep(wait * 1000);
return runOnce(agent, input, attempt + 1);
}
return res.json();
} catch (err) {
// A timeout here means the run may still be going. Do NOT retry -
// record it and let a human decide.
if (err.name === "TimeoutError") throw new Error("Run timed out; it may still be charged");
throw err;
}
}Guarding a budget
The two brakes that exist are your balance and the rate
limit. Add a third of your own: keep the
working balance small, top up deliberately, and stop your job when
credits_remaining drops under what the next run needs.