inklet docs

Timeouts and retries

How the inklet SDK bounds every request in time, cancels calls with an AbortSignal, and which failures it retries for you.

Every request the SDK sends has a timeout, every call can be cancelled, and the calls that only read ride out a flaky network on their own. The calls that create or change something are never retried for you — that is what idempotency keys are for.

Timeouts

Two clocks are set on the client:

const inklet = new Inklet({
  pat: process.env.INKLET_PAT!,
  timeoutMs: 30_000,        // each API request — default 60 s
  uploadTimeoutMs: 600_000, // each binary upload to storage — default 5 min
});
OptionCoversDefault
timeoutMsOne API request, from sending it until its response has been read in full60,000 ms
uploadTimeoutMsOne asset's upload to storage300,000 ms

Uploads get their own clock because they move the file itself: five minutes carries a full 10 MiB asset at about 280 kbit/s. An upload that runs out of time is a failed upload like any other, so it is refreshed and tried once more before AssetUploadError.

A request that runs out of time throws RequestTimeoutError — a NetworkError with code: "request_timed_out" and the timeoutMs that applied.

Per call

Every method that talks to inklet takes { signal, timeoutMs } as its last argument, or inside the options object it already has:

const controller = new AbortController();

const page = await inklet.displays.list({ limit: 20, signal: controller.signal });
const display = await inklet.displays.retrieve(page.items[0].id, { timeoutMs: 5_000 });
await inklet.push.auto({ assets }, { timeoutMs: 20_000 });

A per-call timeoutMs applies to each request the call sends, not to the call as a whole — a push sends several. For an overall limit, pass AbortSignal.timeout(ms) as the signal:

await inklet.push.auto({ assets }, { signal: AbortSignal.timeout(45_000) });

A storage upload keeps the client's uploadTimeoutMs whatever the call says.

Cancelling

Aborting a signal cancels the request in flight — including inside the waiting helpers, watch(), and timeline() — and the call rejects with OperationAbortedError, with the signal's reason as its cause.

Neither an abort nor a timeout undoes anything inklet had already accepted. A push aborted after its upload may have stored the Content; retry it with the error's idempotencyKey rather than a fresh one.

Waiting helpers

analyses.wait(), presentations.waitUntilReady(), contents.waitUntilReady(), and displays.waitUntilCurrent() take their own timeoutMs, and it is a hard deadline for the whole wait: a read still in flight when it runs out is cancelled. The wait then throws OperationTimeoutError, and the thing you were waiting for carries on — read it again later to see it finish.

Streams

On analyses.watch() and requestRaw(), timeoutMs only covers getting the response headers. A stream that runs for minutes is not cut off by it; stop one with signal, or by breaking out of the for await loop.

What is retried for you

The calls that only read — the waiting helpers, timeline(), and watch()'s polling fallback — ride out up to three transient failures in a row:

  • a dropped connection or a request timeout
  • HTTP 408, 429, 500, 502, 503, or 504

They back off from the poll interval, doubling up to 30 seconds, and never retry sooner than the response's Retry-After. A 429 that means a spent allowance (quota_exceeded) is not retried, and neither is a Retry-After longer than 60 seconds: that error is thrown with retryAfterMs set, so you can decide.

watch() reconnects on the same failures, up to five attempts in a row, resuming from the last event it delivered.

What is not

Everything that creates or changes something — contents.upload() and .create(), analyze(), direct(), push.*, presentations.generate() and .render(), displays.setCurrent() and .advance() — is sent once. If it fails, you decide:

ErrorRetry?
ConfigurationErrorNo — fix the call.
NetworkError, RequestTimeoutErrorYes, with error.idempotencyKey where there is one.
RateLimitError, code: "rate_limited"Yes, after error.retryAfterMs.
RateLimitError, code: "quota_exceeded"Not until details.resetAt, or an upgrade.
ApiError with status 500–504Yes, with error.idempotencyKey.
Anything else from the APIOnly once the cause is fixed.

displays.advance() has no idempotency key, and a retry after a lost response would skip a second image. Read the display first and check pendingPresentationId before calling it again.

On this page