Retries, timeouts and idempotency

What the SDK retries, how it avoids doing anything twice, and how to control it.

Networks fail and servers get busy. The SDK retries what is safe to retry, so a brief outage does not reach your code. It never makes a request run twice.

What is retried

The SDK retries a request that is safe to repeat (see Never twice) up to maxRetries times, 2 by default, when:

  • it got no answer: a network error, a DNS or TLS failure, or the timeout ran out;
  • the API answered 408, 500, 502, 503 or 504;
  • the API answered 429 and said how long to wait (Retry-After);
  • the API answered 409 with the code idempotency_in_progress: an earlier attempt with the same idempotency key is still running. The SDK asks again after 5 seconds.

Between attempts, the SDK waits 0.5 s, then 1 s, then 2 s, doubling up to 8 s. It adds some randomness so that many clients don't retry at the same moment. When the API says how long to wait (Retry-After), the SDK waits exactly that, if it is a minute or less. If the wait is longer, it throws the error at once for you to schedule.

A 429 without Retry-After is thrown at once. It comes from a sender's limit or from LinkedIn throttling the sender: retrying within seconds would not help, and with LinkedIn it makes things worse. Other 4xx answers are not retried: the same request would fail the same way.

Never twice

Retrying is only safe if the retry cannot repeat the action. The SDK sorts every method into one of three groups:

  • Reads, updates and deletes are retried as they are. Doing them twice gives the same result as doing them once.
  • The endpoints that take an Idempotency-Key: one-off LinkedIn actions (messages, InMails, invitations, profile visits), sending an email or an inbox reply, and mailbox orders. The SDK generates a key and sends it with every attempt. The API answers a retry with the first result, so nothing is sent or charged twice. All methods marks these methods Retry-safe.
  • Any other POST, such as creating a campaign or adding leads, is not retried. After a dropped connection it may have gone through, so the SDK throws and you decide.

Your own idempotency keys

The SDK's keys protect only the retries within one call. Your own code may repeat a call too: a job runs again after a crash, or a queue delivers a message twice. To make those repeats safe, pass a key derived from your data:

await swarmhit.accounts.messages.send(
  senderId,
  { profileUrl: lead.profileUrl, text: `Welcome aboard, ${lead.firstName}!` },
  { idempotencyKey: `welcome-${lead.id}` },
);

A key is scoped to the endpoint and the account (or chat) it acts on, and kept for 24 hours. When you send a key again:

  • The first attempt succeeded: the API returns its result instead of sending a second message. The body is not compared, so a reused key replays the first response even if the body differs: use a new key for a new message.
  • The first attempt is still running: the API answers 409 idempotency_in_progress, and the SDK waits and asks again.
  • The first attempt was refused with a 4xx: the key is freed, and the request runs again.
  • The first attempt ended in a server error, such as LinkedIn failing mid-way: the API answers 409 idempotency_failed rather than risk a duplicate. Check whether the action went through before you try again with a new key.

A send can outlast the timeout: LinkedIn is slow at times, and the API keeps going after the SDK gave up. If it still runs when the retries are used up, the error carries the key. Ask again with it later, and the API answers with the first attempt's outcome instead of sending twice:

try {
  await swarmhit.accounts.messages.send(senderId, { profileUrl, text });
} catch (err) {
  if (err instanceof SwarmhitError && err.idempotencyKey) {
    // later: the same call, with { idempotencyKey: err.idempotencyKey }
  }
}

Timeouts

timeout is the time one attempt may take: 60 seconds by default. The SDK retries a request that times out, like a network error, when it is safe to. So a call can take up to about timeout × (maxRetries + 1), plus the waits between attempts.

const swarmhit = new Swarmhit({ timeout: 20_000, maxRetries: 3 });

Request options

Every method takes these as its last argument, for that one request:

await swarmhit.accounts.getStats(accountId, { days: 90 }, {
  timeout: 120_000, // ms per attempt
  maxRetries: 0, // fail fast
  signal: AbortSignal.timeout(30_000), // cancel the whole call, retries included
  idempotencyKey: 'my-key', // on endpoints that accept one
  headers: { 'X-Correlation-Id': correlationId },
  workspace: 'ws_123', // see Organizations
});

A signal cancels the call wherever it is, including between retries. The call then rejects with APIUserAbortError.

On this page