11.1 fetchLater(): deferred fetching, from the specification
The real FetchLaterResult IDL, the five throwing gates, whole-request quota accounting against the per-origin cap, and when deferred beats immediate.
Advanced
The newest member of the family does not send at all — it
schedules.
fetchLater(), defined in the Fetch standard's deferred fetching section, parks a request with the browser and lets the browser decide when firing it is cheapest: typically at the end of the page's life, or after a delay, or when the browser believes scripts are about to lose their chance to run. It is the first API in this series whose queueing is the
primary feature rather than a side effect of fire-and-forget.
The real IDL and the real return value
// Fetch standard, deferred fetching — verbatim:
dictionary DeferredRequestInit : RequestInit {
DOMHighResTimeStamp activateAfter;
};
[Exposed = Window]
interface FetchLaterResult {
readonly attribute boolean activated;
};
partial interface Window {
[NewObject, SecureContext]
FetchLaterResult fetchLater(RequestInfo input,
optional DeferredRequestInit init = {});
};
The return value is a
FetchLaterResult whose single
activated getter reports whether the deferred request has fired — an observation handle, not a control handle. There is no
dispatch(), no options bag beyond
DeferredRequestInit (a
RequestInit plus exactly one new member,
activateAfter), no Worker exposure (
[Exposed = Window]), and a secure-context requirement. Anything broader you may have read about this API — background options, worker support, void returns — describes an API that does not exist in the specification.
The throwing gates, in spec order
The method steps run five synchronous checks before anything is queued, and their order is worth internalizing:
activateAfter < 0 throws
RangeError (negative delays are nonsense, and zero is legal — immediately eligible); a document that is not fully active throws
TypeError; a non-HTTP(S) or untrustworthy URL throws
TypeError; a body whose length is unknown — a
ReadableStream — throws
TypeError, with the spec's own gloss:
"Requests whose body is a ReadableStream object cannot be deferred."; and finally the quota check throws
QuotaExceededError when the available quota is less than the request's total length. All five happen at call time — none of this API's failures are deferred like its requests.
A different quota — and a different accounting
Here the API sharply departs from everything this series taught about 64 KiB. The deferred-fetch quota counts the
whole request: the serialized URL, the referrer, every header name and value, and the body — the spec's
compute the total request length algorithm sums them all. So a URL alone can exhaust the per-origin cap, something sendBeacon's body-only pool would never do. The architecture around it: 640 kibibytes per top-level traversable (a tab), of which 128 KiB is by default delegated for cross-origin nested documents reserving 8 KiB each, and — the number that bounds any single origin's traffic — at most 64 KiB
concurrently per reporting origin. Two honest labels attach to all of this: the quota section is marked non-normative in the specification (engines may vary), and pending records hold their quota only while unsent — once processed, the bytes are released.
The runnable example implements both algorithms faithfully — the total-length sum and the pending-aware quota getter — and replays the specification's own worked examples: the too-long URL that throws on its own, the two 64-KiB bodies to different origins that both queue, and the third request back to the first origin that throws because that origin's cap is spent:
Output
Ready to run.
When it fires — and when it does not replace sendBeacon
With
activateAfter unset, a deferred fetch is processed when its fetch group is terminated — the page goes away. That is the park-until-the-end semantic: telemetry you want sent eventually but not urgently, moved off your critical path entirely. With
activateAfter set, the wait also ends early on two conditions: the delay elapses, or the user agent has reason to believe it is about to lose the opportunity to execute scripts — backgrounding, long-hidden documents. Note the asymmetry against sendBeacon: the Beacon spec's MUST-flush-on-hidden has no equivalent here, and without
activateAfter there is no background trigger at all — only termination. The spec's own usage examples, verbatim in shape:
// Park a report until the document is terminated:
fetchLater("https://report.example.com", {
method: "POST",
body: JSON.stringify(myReport),
headers: { "Content-Type": "application/json" },
});
// Park it for at most 5 seconds — and observe activation:
const result = fetchLater("https://report.example.com", {
method: "POST",
body: JSON.stringify(myReport),
headers: { "Content-Type": "application/json" },
activateAfter: 5000,
});
function check_if_fetched() { return result.activated; }The second example carries a guarantee worth pausing on: the request is invoked even where the user agent throttles timers, because the wait is not a
setTimeout — it is part of the browser's own scheduling. A
FetchLaterResult also composes with
AbortSignal (pass
signal in the init; aborting marks the record aborted). Browser support is young and uneven — feature-detect
typeof window.fetchLater before relying on it, exactly as the example does, and treat per-engine shipping details as moving targets rather than facts.
Quick check
How does deferred-fetch quota accounting differ from sendBeacon's 64 KiB pool?
// fetchLater quota counts:
Key takeaways
-
fetchLater returns
FetchLaterResult(anactivatedgetter) — Window-only, secure-context, no dispatch method, no worker exposure. -
Five synchronous gates:
RangeErrorfor negative activateAfter;TypeErrorfor not-fully-active, non-HTTP(S)/untrustworthy URLs, and stream bodies;QuotaExceededErrorfor quota. - The quota counts the whole request — URL, referrer, headers, body — capped at 64 KiB concurrently per reporting origin; the numbers section is non-normative.
-
Unset
activateAfter= termination-only firing; set, it also fires on background/long-hidden or the delay — with timer-throttle immunity. -
Deferred requests force
keepalive: true(shared pool) andservice-workers mode "none"(invisible to workers). Feature-detect before use.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.