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.

Who sends the crash report First, the server responds with a Reporting-Endpoints header naming the default endpoint. Second, the renderer process running the page runs out of memory and dies; no JavaScript runs. Third, the browser process, which survives, queues a crash report with reason oom. Fourth, the browser delivers the batched report as a POST with application/reports+json to the endpoint, where it is stored and aggregated. 1. Server Reporting-Endpoints: default="…/reports" 2. Renderer dies out of memory no JS, no beacon 3. Browser process queues type: crash body.reason: "oom" 4. Your endpoint POST, batched, reports+json [{ "type": "crash", "age": 42000, "url": "https://app.example.com/editor/123", "user_agent": "Mozilla/5.0 … Chrome/… ", "body": { "reason": "oom" } }]

Step-by-Step Fix

  1. 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 be default for crash reports. Verification: DevTools → Application → Reporting API lists the default endpoint for the page.
  2. Build a receiving endpoint. Accept POST requests with Content-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 with curl stores a row.
  3. 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" and body.reason: "oom".
  4. 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.
  5. 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.
  6. 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.
OOM crash rate by route, before and after a fix Before the fix, the editor route has 38 OOM crashes per ten thousand page views, the dashboard route 6, and the settings route 1. After fixing a leak in the editor, its rate falls to 4 per ten thousand while the others stay the same. OOM crashes per 10,000 page views /editor/:id 38 before 4 after /dashboard 6 6 /settings 1 1

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.

Reading daily OOM report volume Express OOM reports as a rate per page view by route and release. A stable rate is healthy. A sudden drop to zero usually means the Reporting-Endpoints header disappeared or the endpoint started failing. A rise concentrated on one route and release points to a regression, allowing for reports that arrive late via the age field. OOM reports per page view, by route and release Healthy: pipeline and release both fine stable Header missing or endpoint failing; check delivery, not memory sudden drop to zero Regression; allow for late reports (age field) before judging rise on one route

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.