Axios retry: retry a POST safely with an idempotency key

Use axios-retry for network errors, 429 and 5xx with exponential backoff, and retry a paid POST only when it carries an Idempotency-Key.

5 min readSume
All posts

To retry Axios requests, install axios-retry and call axiosRetry(client, { retries, retryCondition, retryDelay }): a failed request that matches retryCondition is sent again after retryDelay. Axios's own docs show the same idea written by hand as a response interceptor. Against a paid API, retry network errors, 429, and 5xx with exponential backoff, and retry a POST only when it carries an Idempotency-Key, so a resend returns the original job instead of starting and billing a second one.

axios-retry facts come from its README and source, Axios facts from its Retry and error recovery, Error handling, and Request config pages, and Sume facts from Errors and rate limits, Errors and spend, and Create a run. All were read on 2026-09-27. Axios reaches Sume over plain HTTPS; there is no Sume plugin for it.

What does axios-retry retry by default?

More than you may want for a paid create. The default condition, isNetworkOrIdempotentRequestError, accepts network errors on any method, POST included, apart from codes it treats as unsafe to retry, plus a 429 or 5xx on GET, HEAD, OPTIONS, PUT, or DELETE. It never retries a timed-out request (ECONNABORTED), and it skips a canceled POST. By default the resends go out with no delay, although every built-in delay function takes the larger of its own value and the Retry-After header.

From the axios-retry README and source, read 2026-09-27.
OptionDefaultEffect
retries3Retries before failing
retryConditionisNetworkOrIdempotentRequestErrorNetwork errors on any method; 429 and 5xx on idempotent methods only
retryDelayNo delayexponentialDelay or linearDelay add backoff
shouldResetTimeoutfalseThe request timeout covers the whole lifecycle, not each retry

Why is retrying a POST risky, and what makes it safe?

A network error or a timeout does not tell you whether the server got the request. If it did, a blind resend of a create starts a second paid job, which is why Sume's docs say not to retry unsafe submit requests without an Idempotency-Key. With a key, the resend is safe:

  • Same key, same body: 200 with the original receipt and idempotency_hit: true. No second run, no second charge.
  • Same key while the first request is still in flight: 409 idempotency_key_in_use, retryable; wait about a second and resend.
  • Same key after a create that failed with 402, 503, or similar: the key was released, so once the cause is fixed, a retry with the same key can start the run.
  • Derive the key from the thing being made, such as an order id plus a version, not from the moment of asking. axios-retry resends the same request config, so every attempt carries the same key.

How do I write the retry condition?

Refuse POSTs without a key first. Then retry what has no response, except a request you canceled yourself. When a response carries Sume's error envelope, trust its retryable flag; otherwise fall back to 408, 429, and 5xx, the same set Sume's own SDK retries. The delay reuses exponentialDelay with a 500 ms factor, about 1, 2, and 4 seconds plus up to 20% jitter, and waits longer when Retry-After or the envelope's retry_after_seconds asks. Send the key on each create, for example { headers: { "Idempotency-Key": "order-1042-promo-v1" } } as the third argument of sume.post.

import axios from "axios";
import axiosRetry, { exponentialDelay } from "axios-retry";

const sume = axios.create({
  baseURL: "https://api.sume.com",
  timeout: 30_000,
  headers: { Authorization: `Bearer ${process.env.SUME_API_KEY}` },
});

axiosRetry(sume, {
  retries: 3,
  shouldResetTimeout: true, // every attempt gets the full 30 s
  retryDelay: (count, error) => {
    const hint = error.response?.data?.error?.retry_after_seconds ?? 0;
    return Math.max(exponentialDelay(count, error, 500), hint * 1000);
  },
  retryCondition: (error) => {
    const { config, response } = error;
    if (config?.method === "post" && !config.headers?.["Idempotency-Key"]) return false;
    if (!response) return error.code !== "ERR_CANCELED"; // network error or timeout
    const envelope = response.data?.error;
    if (typeof envelope?.retryable === "boolean") return envelope.retryable;
    return response.status === 408 || response.status === 429 || response.status >= 500;
  },
});

Which errors should not be retried automatically?

Most 4xx answers. A 4xx at create means nothing ran and nothing was charged, so fix the call; Sume's docs call a 403 insufficient_scope retried in a loop the most common and most expensive mistake.

  • 402 insufficient_credits: top up first; retrying returns the same answer.
  • 409 idempotency_conflict: the key was already used with a different body. Fix the key derivation.
  • 502 attachment_fetch_failed: despite the 5xx, it is your input; the API reference example shows retryable: false and next_action: fix_input. A rule based on status codes alone would resend it, so read the envelope before the status.
  • Canceled requests: Axios marks them ERR_CANCELED, and nobody is waiting for the answer.

Does a retry fix an Axios network error in the browser?

Not always. Axios's docs say that in a browser, ERR_NETWORK can also be a CORS or mixed-content policy violation, and resending the same request does not change the policy. For a paid API the question is moot: Sume's docs keep API keys out of frontend JavaScript, so this client belongs on your server. CORS error calling the Sume API from a browser explains the fix.

On the server, Sume's TypeScript SDK makes axios-retry unnecessary: createSumeClient already retries 408, 429, 5xx, and transport failures twice by default, with exponential backoff and jitter, honors retry-after, and retries a POST only when it carries an Idempotency-Key; see the Sume TypeScript SDK quickstart.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume