p-retry npm: retry a paid API POST and stop on errors

p-retry reruns an async function with exponential backoff. Throw AbortError on answers a resend can't fix, and keep one Idempotency-Key per job.

5 min readSume
All posts

p-retry is an npm package that calls an async function again when its promise rejects, with exponential backoff: await pRetry(run, { retries: 5 }). To stop early, throw AbortError from inside the function; p-retry then rejects at once without another attempt. For a paid API, throw AbortError on answers a resend cannot fix (400, 401, 402, 403, a 409 conflict), let network errors, 429 and 5xx retry, and create the Idempotency-Key outside the function so every attempt sends the same one.

p-retry facts come from its README. The example API is Sume's Format run create, from Create a run, Errors and spend and Errors and rate limits, all read on 2026-09-29. It is a plain HTTPS call with fetch; there is no Sume plugin for p-retry.

What does p-retry do by default?

It retries any rejection up to 10 times, starting 1 second after the first failure and doubling each time, with no upper bound on the gap. At those defaults the tenth retry waits 512 seconds, so set retries and maxTimeout for anything a user is waiting on. p-retry does not retry most TypeErrors, except network errors, which is what fetch throws when the connection fails.

From the p-retry README, read 2026-09-29.
OptionDefaultWhat it does
retries10Maximum number of retries
factor2Exponential factor
minTimeout1000Milliseconds before the first retry; 0 retries at once
maxTimeoutInfinityLongest gap between two retries, in milliseconds
randomizefalseMultiplies each wait by a factor between 1 and 2
maxRetryTimeInfinityTotal time the retried operation may run

Why must the Idempotency-Key live outside the function?

p-retry calls your function again from the top. If the function builds the key with crypto.randomUUID(), each attempt sends a new key, and a request that reached the server before the connection dropped starts a second paid run. Build the key once, outside the function, from the thing being made (an order id and a version), so every attempt carries the same one. Sume's docs say not to retry unsafe submit requests without an Idempotency-Key; with the same key and the same body, a resend returns the original run instead of a second charge. Idempotency keys for AI video APIs covers every replay case.

How do I stop p-retry on errors that won't succeed?

Read the error envelope and throw AbortError when a resend can't help. Sume's error body carries retryable and next_action, and the docs say a 4xx at create means nothing ran and nothing was charged, so fix the call rather than retrying it. onFailedAttempt can return a promise, which the README uses to add a delay; here it waits out retry-after on a 429.

import pRetry, { AbortError } from "p-retry";

const key = "order-8823-clip-v1"; // fixed for every attempt

async function createRun() {
  const res = await fetch("https://api.sume.com/v1/formats/acme/product-clip/runs", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": key,
    },
    body: JSON.stringify({ input: { product_url: "https://example.com/p/8823" } }),
  });
  const body = await res.json().catch(() => ({}));
  if (res.ok) return body.data; // 202 new run, 200 replay
  const err = body.error ?? {};
  const msg = `${res.status} ${err.code}`;
  if (err.code === "idempotency_key_in_use" || res.status === 429 || res.status >= 500) {
    if (err.retryable === false || err.next_action === "fix_input") throw new AbortError(msg);
    throw Object.assign(new Error(msg), { wait: Number(res.headers.get("retry-after")) || 0 });
  }
  throw new AbortError(msg); // 400, 401, 402, 403, 404, 409 conflict
}

const run = await pRetry(createRun, {
  retries: 4, maxTimeout: 30_000, randomize: true,
  onFailedAttempt: ({ error }) => new Promise((r) => setTimeout(r, (error.wait ?? 0) * 1000)),
});

Which Sume answers should abort the retry?

These come from the Format run create table. The API key stays in a server-side environment variable; Sume's docs keep keys out of frontend JavaScript.

From Errors and spend and Errors and rate limits, read 2026-09-29.
Answerp-retry actionWhy
400 invalid_requestAbortErrorThe body is invalid
402 insufficient_creditsAbortErrorRetrying without a top-up returns the same answer
403 insufficient_scopeAbortErrorMint a new key with the scopes
409 idempotency_conflictAbortErrorKey reused with a different body
429 rate_limitedRetry after retry-afterThe key's write budget is spent
502 attachment_fetch_failedAbortErrorDespite the 5xx, next_action is fix_input
503 studio_agent_upstream_unavailableRetryRetry later with the same Idempotency-Key

Does p-retry wait for the job to finish?

No, and it shouldn't. The create answers 202 with a run id and a status_url as soon as the run exists; generation continues after that. Wrap only the create in p-retry, then poll the status URL on its own schedule. To cap how many runs you start at once, pair it with p-limit. Axios retry covers the same rules for Axios, and fetch timeout and retry adds a per-attempt timeout.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume