Trigger.dev retries and Sume errors: skip 4xx, wait on retry-after

Trigger.dev retries any uncaught throw. Use catchError to skip Sume errors that cannot succeed, and read the retry-after header on a 429.

4 min readSume
All posts

Trigger.dev retries a task whenever it throws, which is wrong for a Sume 400 or 402 that will fail the same way every time. Use catchError to skip those, and use the retry-after header on a 429, because Sume's rate-limit headers do not match the names in Trigger.dev's own retry.fetch example.

Trigger.dev facts are from its Errors and retrying docs; Sume facts from Errors and credits, Authentication and Generation admission, read 2026-10-01.

What does Trigger.dev do on an error?

The docs say an uncaught thrown error triggers a retry per your config. AbortTaskRunError disables retrying, retry.onThrow() retries a code block, and catchError can override the decision, for example by returning { skipRetrying: true }. The retry.fetch() example handles 429 with a headers strategy that reads x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset.

Which Sume errors should not retry?

Sume's 429 carries retry-after, ratelimit-limit, ratelimit-remaining and ratelimit-reset, with no x- prefix, and error.details.scope says read or write. Because the names differ, Trigger.dev's documented header example would not find them. Read retry-after yourself.

Sume statuses and a retry decision, from the Sume docs read 2026-10-01
Status and codeMeaningRetry in the task
400 invalid_requestBad body or headersNo, fix the request
401 unauthorizedKey missing or invalidNo
402 insufficient_creditsCannot reserve costNo
404 not_foundUnknown model or resourceNo
429 rate_limitedRequest budget usedYes, after retry-after
429 queue_fullNo accepted capacity leftYes, later
503 provider_capacity_exceededCannot dispatch safelyYes, check retryable

How do you code the skip?

Throw a typed error that carries the HTTP status, and return skipRetrying for the codes that cannot succeed. Everything else keeps Trigger.dev's normal backoff.

import { task } from "@trigger.dev/sdk";
import { createSumeClient, generateVideoV1 } from "@sume-com/sdk";

class SumeHttpError extends Error {
  constructor(public status: number, message: string) { super(message); }
}
const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });

export const submit = task({
  id: "sume-submit",
  retry: { maxAttempts: 5 },
  run: async (p: { key: string; prompt: string }) => {
    const { data, error, response } = await generateVideoV1({
      client,
      headers: { "idempotency-key": p.key },
      body: { prompt: p.prompt, mode: "async" },
    });
    if (error) throw new SumeHttpError(response.status, JSON.stringify(error));
    return data!.data.request_id;
  },
  catchError: async ({ error }) => {
    const dead = [400, 401, 402, 404, 413, 415];
    if (error instanceof SumeHttpError && dead.includes(error.status)) {
      return { skipRetrying: true };
    }
  },
});

Does the wait come from Trigger.dev or Sume?

Trigger.dev's backoff sets the spacing unless you sleep yourself, and retry-after is Sume's hint for how long that should be. A 429 means the budget for that scope is spent: writes are 120 a minute on Free up to 1,200 on Scale, and reads are forty times that. See the retry-after post.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume