AbortSignal.any: stop a Sume request on SIGTERM or after 5 seconds

Combine a shutdown signal and AbortSignal.timeout with AbortSignal.any, then tell a timeout from a SIGTERM stop on a Sume GET /v1/me call in Node.

4 min readSume
All posts

Pass AbortSignal.any([shutdown.signal, AbortSignal.timeout(5000)]) as the signal of your fetch call. The request then ends on whichever happens first: your process receives SIGTERM, or five seconds pass. The two cases fail differently, so check error.name === "TimeoutError" for the timer and your own controller for the shutdown. The MDN page describes any() as returning a signal that aborts when any of the given signals abort.

The example is a key check against GET /v1/me, the cheap read documented on the authentication page. The same pattern fits a status poll in a worker that gets deploy signals. We ran it on Node 22.14 against a local stand-in server: one fast reply, one silent server that triggered the timeout, and a silent server that received SIGTERM after one second.

Why separate the two

A timeout means the answer is late, so a retry may be right. A shutdown means the process is ending, so a retry is wrong and the caller should exit. Treating both as one generic failure either retries during shutdown or exits on a slow network.

What each abort looks like (read 2026-10-08)
CauseHow it showsNext action
AbortSignal.timeout fireserror.name is TimeoutErrorretry or report slow
Your controller abortsyour signal is aborted, error is the reason you passedstop, do not retry
Server answersHTTP status such as 200 or 401use the status

Steps

  • Create one AbortController for shutdown at process start.
  • Call shutdown.abort() from a SIGTERM handler.
  • For each request, build a new AbortSignal.any with a fresh AbortSignal.timeout, because a timeout signal cannot be reset.
  • In the catch block, check the timer first, then your controller.
  • Keep the key in SUME_API_KEY and refuse to start if it is empty.

Sample

Run it as SUME_API_KEY=... node check.mjs. Top-level await needs an ES module, which .mjs provides.

const key = process.env.SUME_API_KEY ?? "";
const base = process.env.SUME_BASE ?? "https://api.sume.com";
if (!key) throw new Error("SUME_API_KEY is empty");
const shutdown = new AbortController();
process.once("SIGTERM", () => shutdown.abort(new Error("shutdown")));

export async function checkKey() {
  const signal = AbortSignal.any([shutdown.signal, AbortSignal.timeout(5000)]);
  try {
    const r = await fetch(`${base}/v1/me`, { headers: { authorization: `Bearer ${key}` }, signal });
    return r.status;
  } catch (e) {
    if (e.name === "TimeoutError") return "timed out after 5 s";
    if (shutdown.signal.aborted) return "stopped by SIGTERM";
    throw e;
  }
}
console.log(await checkKey());

Edge cases to know

AbortSignal.timeout counts from the moment you create it, so build it inside the function, not at module load. Otherwise the second call in a long-lived process would already be expired. Also remember that a timed-out request tells you nothing about the server: a GET is safe to repeat, but a POST that creates a job needs an Idempotency-Key so the retry returns the same job instead of making a second one.

On shutdown, prefer a short drain. Stop starting new requests, abort the ones in flight, flush your logs and exit with a clear code. The sample returns a readable string for each outcome so you can see which path ran.

What Sume does not do

Aborting a fetch only stops your client from waiting. For a submit call, the server may already have accepted the work, so use an Idempotency-Key on retries and cancel queued jobs explicitly if you no longer want them. Sume does not infer your intent from a closed connection.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume