ky does not retry POST by default: Sume submit with a key

ky retries GET but not POST, and it also retries 413. How to configure it so a Sume submit retries safely and a too-large body fails fast.

5 min readSume
All posts

Short answer

ky retries at most 2 times by default and does not include POST in its default retry methods, per the ky README. To retry a Sume submit you must add post to methods and send an Idempotency-Key. You should also drop 413 from the retried status codes, because a body that was too large once is still too large.

ky defaults versus what Sume needs

ky's defaults are tuned for idempotent calls, which is why POST is left out. That is a sensible default and matches the Sume SDK, which retries POST only when an Idempotency-Key is present. The risk is in the status list: ky also retries 413.

Sume answers 413 with payload_too_large when the body exceeds the API limit. Resending the same body repeats the failure and delays the real error.

ky retry defaults, per its README (read 2026-10-03)
OptionDefault
limit2
methodsget, put, head, delete, options, trace, query
statusCodes408, 413, 429, 500, 502, 503, 504
afterStatusCodes (honor Retry-After)413, 429, 503
Delay0.3 x 2^(attempt-1) x 1000 ms, backoffLimit Infinity

Configuration

Create one client, list the methods you want, and leave 413 out. Generate the key once per logical submit, outside the retry. ky honors the Retry-After header on 429 and 503, which lines up with the retry-after header Sume sends on rate_limited and queue_full.

Note that backoffLimit is Infinity by default, so set a cap if you raise limit. The Sume SDK caps its own computed backoff at 8 seconds and, when a retry-after header is present, waits that long instead (up to 60 seconds).

import ky from "ky";

const sume = ky.create({
  headers: { "x-api-key": process.env.SUME_API_KEY! },
  retry: {
    limit: 3,
    methods: ["get", "head", "post"],
    statusCodes: [408, 429, 500, 502, 503, 504],
    backoffLimit: 8000,
  },
});

const key = crypto.randomUUID();
const job = await sume
  .post("https://api.sume.com/v1/images", {
    json: { model: "sume/auto", prompt: "A matte black bottle on marble", mode: "async" },
    headers: { "Idempotency-Key": key },
  })
  .json();
console.log(job);

What the retry will and will not fix

A retry with the same key and the same body returns the original job instead of billing twice. A different body under the same key on a Format run is 409 idempotency_conflict, which ky does not retry because 409 is not in the list. Concurrent duplicates give 409 idempotency_key_in_use, which is retryable; add 409 only if you inspect the error code first.

Do not use the retry to wait for a job. Retries cover the submit. After the 202, poll the status endpoint and honor next_poll_after_seconds. See the errors page for the status codes.

Polling with the same client

The same client can poll. GET is in the default method list, so a transient 503 or a dropped connection on a status read is retried without extra setup. Keep the loop itself in your code: read next_poll_after_seconds from each status reply and sleep for that long, and stop as soon as terminal is true. ky's delay applies between retries of one request, not between polls, so it is no substitute for the loop.

Statuses are queued, processing, completed, failed and canceled. Only completed has a result. Asking for /result on any other state gives 409 job_not_completed, so read the failure from the job record instead.

Timeouts and aborts

Retries multiply wall time. Three retries with a growing delay can take longer than a single attempt by tens of seconds, so set a total deadline in your own code if a user is waiting. If you abort, the job is not canceled; the submit may already have been accepted. Resubmitting with the same key returns the original job, which is why the key must live outside the retry and, ideally, in your own storage next to the intent it represents.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume