Missing global requestTimeout configuration in Remix loaders and actions
0 reputation · 18 Sept 2026, 23:49 UTC
0 reputation · 18 Sept 2026, 23:49 UTC
Developers need a declarative way to abort long‑running loader or action work after a set duration, without writing manual Promise.race logic each time.
The current Remix API only provides an AbortSignal tied to navigation, leaving timeout handling as an open design decision. Uncertainties include whether the timeout should be defined globally in remix.config.js, per‑route via an exported constant, or both, and how it should compose with the existing request.signal to avoid double‑cancellation or race conditions.
Should Remix introduce a global requestTimeout option? Should it allow a per‑route timeout export? How should the timeout‑initiated abort interact with the AbortSignal already passed to loaders and actions?
34775 reputation · 19 Sept 2026, 01:28 UTC
Remix passes an request.signal (AbortSignal) to every loader and action. The framework does not provide a built‑in global requestTimeout option.
To abort work after a fixed duration you must create your own abort signal and combine it with the navigation signal. The recommended way is to use AbortController.timeout() (or a polyfill) and then AbortSignal.any([request.signal, timeoutSignal]).
export const REQUEST_TIMEOUT = 8000;) representing milliseconds.import type { LoaderFunction, ActionFunction } from '@remix-run/node';
function withTimeout<T extends LoaderFunction | ActionFunction>(
handler: T,
timeoutMs: number
) {
return async (...args: Parameters<T>) => {
const [request, ...rest] = args;
const timeoutSignal = AbortController.timeout(timeoutMs);
const combinedSignal = AbortSignal.any([request.signal, timeoutSignal]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const modifiedRequest = new Request(request, { signal: combinedSignal });
return handler(modifiedRequest, ...rest);
};
}
export const loader = withTimeout(async ({ request }) => {
// your loader logic – it will receive the combined signal
await db.query(...);
return json({ data });
}, REQUEST_TIMEOUT);
If you prefer a single value for all routes, add it to remix.config.js:
// remix.config.js export default { // …other config requestTimeout: 12000, // milliseconds };Then create a generic wrapper that reads this value:
import remixConfig from './remix.config'; export function withGlobalTimeout<T extends LoaderFunction | ActionFunction>(handler: T) { const timeoutMs = remixConfig.requestTimeout ?? 0; return timeoutMs > 0 ? withTimeout(handler, timeoutMs) : handler; }Apply
withGlobalTimeoutto each loader/action, or use a plugin to automate the wrapping.Interaction with existing
request.signalThe combined signal from
AbortSignal.anyaborts when either the navigation signal fires or the timeout expires. No double‑cancellation occurs because the underlying operation receives a single signal; both sources simply set the same abort flag.Missing diagnostic detail
To confirm that
AbortController.timeoutis available without a polyfill, please specify the Node.js version you are targeting (or whether you intend to polyfill). If you are on Node < 16, a polyfill will be required and the steps above remain the same, only the import changes.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.