Detecting Out-of-Memory Crashes with the Reporting API
Users see “Aw, Snap! Out of memory” and your error tracker shows nothing, because a page that has crashed cannot run JavaScript to report its own death. This guide from Measuring Memory in Production Browsers, part of Browser DevTools & Performance Profiling Workflows, shows how to have Chromium deliver crash reports for you through the Reporting API, how to store and triage them, and how to link them to the memory data that explains them.
| Symptom | Root Cause | Immediate Action | Measurable Impact |
|---|---|---|---|
| Users report “Aw, Snap” pages; error tracker is silent | Crashed renderer cannot run JavaScript | Configure a default Reporting endpoint |
Browser-delivered crash reports with a reason |
| No idea which pages crash | No per-URL crash data | Aggregate reports by normalised URL | Ranked list of crashing pages |
| Cannot tell memory crashes from other crashes | Reasons not separated | Filter body.reason === "oom" |
OOM rate tracked separately |
| Crash counts spike after releases | A memory regression shipped | Chart OOM per 10,000 page views by release | Regression visible within hours |
| Reports stop arriving | Header missing on some responses or endpoint failing | Monitor report volume; check endpoint returns 2xx | Continuous coverage |
Root Cause: A Dead Page Cannot Report Itself
When a Chromium renderer process runs out of memory, it terminates. Every page in that process vanishes: no error event, no unhandledrejection, no pagehide, no final beacon. JavaScript-based error tracking, however thorough, is structurally unable to see it. From your telemetry’s point of view the session simply stops.
The Reporting API fixes this by moving responsibility to the browser. A page opts in by sending a Reporting-Endpoints response header that names an endpoint called default. If that page’s renderer later crashes, the browser process — which survives the renderer — queues a report of type crash and delivers it to the endpoint in the background, batched with other reports, as a POST with content type application/reports+json. The report body carries a reason; "oom" means the renderer ran out of memory. Other reasons, such as "unresponsive" for a hung renderer that was killed, exist too, and newer Chromium versions add fields such as whether the crashed frame was top-level and its visibility state when it crashed.
Crash reports do not tell you how much memory was used or which code retained it. What they give you is the outcome — which pages crash, how often, on which browser versions — and that is the metric that matters most to users. Combined with field memory samples from measureUserAgentSpecificMemory, which show how memory grew before the crash, and lab tools that show why, they close the loop. Note that this mechanism is Chromium-specific; iOS Safari kills pages differently, as described in why iOS Safari reloads your tab under memory pressure.
Step-by-Step Fix
- Add the endpoint header. Send
Reporting-Endpoints: default="https://rum.example.com/reports"on document responses for the pages you want covered. The endpoint name must bedefaultfor crash reports. Verification: DevTools → Application → Reporting API lists thedefaultendpoint for the page. - Build a receiving endpoint. Accept
POSTrequests withContent-Type: application/reports+json, parse the JSON array, and store each report with a receive timestamp. Respond with a 2xx status quickly. Verification: sending a hand-crafted sample report withcurlstores a row. - Trigger a test crash on staging. Deploy the header to a staging environment, open a staging page in desktop Chrome, and run a deliberately leaking script that allocates until the renderer is killed (see the code below). Verification: within a few minutes the endpoint receives a report with
type: "crash"andbody.reason: "oom". - Normalise and aggregate. Strip query strings and IDs from URLs, extract the browser version from
user_agent, and count OOM reports per route per day. Verification: a dashboard shows OOM counts by route and release. - Compute a rate. Divide OOM reports by page views for the same route and period (from your analytics). Verification: you have “OOM crashes per 10,000 page views” per route — a number you can set targets for.
- Link to memory telemetry. For the worst routes, look at field memory samples by session age to see how memory grew before the crashes, then reproduce in the lab. Verification: the route with the highest OOM rate also shows the steepest memory growth, and a fix reduces both.
Command and Code Reference
Use case: a minimal Express endpoint that stores crash reports. Keep it tolerant: accept batches, ignore unknown types, answer fast.
// reports-endpoint.js
import express from 'express';
const app = express();
// Reports arrive as a JSON array with this content type
app.use('/reports', express.json({ type: 'application/reports+json', limit: '256kb' }));
app.post('/reports', async (req, res) => {
const reports = Array.isArray(req.body) ? req.body : [];
const rows = reports
.filter((r) => r.type === 'crash')
.map((r) => ({
receivedAt: new Date().toISOString(),
route: new URL(r.url).pathname.replace(/\/\d+/g, '/:id'), // normalise IDs
reason: r.body?.reason ?? 'unknown', // "oom", "unresponsive", …
userAgent: r.user_agent,
ageMs: r.age, // delay before delivery
}));
if (rows.length) await db.insert('crash_reports', rows);
res.sendStatus(204); // quick 2xx
});
app.listen(8080);
Use case: a staging-only page that exhausts memory to test the pipeline. Never ship this; run it on a test route to confirm reports arrive end to end.
// staging/oom-test.js — allocates until the renderer is killed
const hoard = [];
function grow() {
// 50 MB of retained strings per step; the tab will crash within seconds
for (let i = 0; i < 50; i++) hoard.push('x'.repeat(1024 * 1024));
setTimeout(grow, 0);
}
document.querySelector('#start-oom-test').addEventListener('click', grow);
Verification and Regression Prevention
The pipeline works when a deliberate crash on staging produces a stored report within minutes, the daily report volume in production is stable (a sudden drop to zero usually means the header disappeared or the endpoint started failing), and dashboards express OOM as a rate per page views by route and release. Watch the age field: reports can arrive long after the crash, and only when the browser next has network access, so allow for delays before concluding a release is clean.
Make OOM rate a release gate for memory-heavy routes: compare the first days of a release against the previous release’s rate and investigate increases beyond normal variance. Pair each OOM spike with field memory curves from sampling memory telemetry and, for the offending route, with lab analysis using the three-snapshot technique.
Edge Cases and Gotchas
Only the default endpoint receives crash reports
Other report types (CSP, COEP, deprecations) can go to named endpoints, but crash reports are sent to the endpoint called default. If you already use Reporting-Endpoints for other purposes, add a default entry.
Shared processes blur attribution
Several same-site tabs may share one renderer. When it crashes, each affected page may produce a report, and the page that caused the memory growth is not necessarily the one with the most reports. Look at co-occurring URLs in time windows.
Privacy of report contents
Reports include the full page URL and user agent. Strip query strings and identifiers on receipt, and apply the same retention rules as other telemetry.
Other browsers
Firefox and Safari do not deliver crash reports through this mechanism. For them, infer crashes and kills from sessions that end without a pagehide beacon or from unexpected reloads detected via sessionStorage flags.
Frequently Asked Questions
Can I receive crash reports with ReportingObserver in JavaScript?
No. ReportingObserver only sees reports generated while the page is alive, and a crashed page has no running JavaScript. Crash reports are delivered by the browser to a server endpoint configured with the Reporting-Endpoints header.
What other crash reasons might I see?
Besides oom, Chromium can report reasons such as unresponsive when a hung renderer was terminated, and reports with no specific reason for other crashes. Track them separately: unresponsive points at long-running script or infinite loops rather than memory, although a heap near its limit, with the collector running almost continuously, can also make a page unresponsive before it finally runs out of memory.
Should the endpoint be on the same origin?
It can be any HTTPS URL. A same-origin endpoint avoids CORS configuration and keeps reports inside your own infrastructure; a dedicated telemetry domain keeps the traffic away from your application servers. Either way, the endpoint must answer quickly with a success status, because the browser retries failed deliveries and may drop reports that keep failing.
How quickly do crash reports arrive?
Usually within minutes, but delivery is batched and depends on the browser having network access, so some arrive hours later. The report’s age field tells you how long it waited before delivery.
Does a crash report tell me which code leaked?
No. It tells you that a page’s renderer crashed and why (for example oom). Use field memory measurements to see how memory grew during those sessions, and lab tools such as heap snapshots to find the retaining code.
Related
- Measuring Memory in Production Browsers — the parent topic
- Setting Memory Budgets for Low-End Devices — preventing the crashes these reports reveal
- Sampling Memory Telemetry Without Hurting Performance — the growth data that explains crashes
- Browser DevTools & Performance Profiling Workflows — the section overview