Maximum Payload Size Limits for Chrome vs Firefox in Web Push
The Web Push specification (RFC 8030) enforces a 4,096-byte hard ceiling on the encrypted payload body. aes128gcm framing (RFC 8291) consumes at least 38 bytes of that space before a single plaintext character is written, leaving roughly 4,058 bytes of ciphertext room. In practice, safe plaintext caps sit at 3,800 bytes to account for library padding overhead and browser-specific enforcement variance.
Chrome returns an explicit 413 Request Entity Too Large when the limit is breached. Firefox may return 400 Bad Request or silently drop the message depending on the overflow magnitude. Both enforce the same 4 KB ciphertext ceiling; the difference is in error visibility.
Cryptographic Overhead Breakdown
aes128gcm encoding (the mandatory content-encoding per Push API Payload Encryption) adds a deterministic framing envelope to every push message. Understanding its exact byte cost is the foundation of any payload budget calculation.
| aes128gcm Frame Component | Bytes |
|---|---|
| Salt | 16 |
| Record-size field | 4 |
| Key-ID length field | 1 |
| Key-ID (empty for anonymous senders) | 0 |
Padding delimiter byte (0x02) |
1 |
| AES-128-GCM authentication tag | 16 |
| Minimum framing total | 38 |
Libraries such as web-push add variable-length padding beyond this minimum to obscure payload length. Target a 3,800-byte plaintext cap rather than the mathematical 4,058-byte ceiling to absorb that variance.
Per-Browser Enforcement Matrix
All major push services enforce the same 4,096-byte ciphertext ceiling mandated by RFC 8030. The observable difference is in how each browser’s push service surface reports violations.
| Browser / Environment | Ciphertext Limit | Recommended Plaintext Cap | Overflow Response |
|---|---|---|---|
| Chrome 90+ (FCM endpoint) | 4,096 bytes | 3,800 bytes | 413 Request Entity Too Large |
| Firefox 90+ (autopush) | 4,096 bytes | 3,800 bytes | 400 Bad Request or silent drop |
| Safari iOS 16.4+ / macOS 13+ | 4,096 bytes | 3,800 bytes | 413 Request Entity Too Large |
Chrome and Safari produce explicit 413 responses that your server-side retry logic can detect immediately. Firefox’s silent-drop path is more dangerous — it returns 201 Created to your server but never delivers to the device, making the failure invisible without client-side telemetry. See the cross-browser notification quirks reference for a broader treatment of Firefox-specific delivery edge cases.
Diagnostic Workflow: Isolating 413 Errors and Silent Drops
Work through this sequence to identify exactly where oversized payloads fail.
-
Measure UTF-8 byte length before encryption. Never rely on character count; multi-byte Unicode characters inflate byte length silently.
const byteLength = new TextEncoder().encode(JSON.stringify(payload)).length; if (byteLength > 3800) { throw new Error(`Payload too large: ${byteLength} bytes — max safe plaintext is 3,800 bytes`); } -
Log ciphertext size in the service worker. Intercept the
pushevent and recordevent.data.byteLength. If the value reported bybyteLengthafter decryption is consistently lower than expected, the push service silently truncated rather than rejected.self.addEventListener('push', (event) => { const ciphertextSize = event.data ? event.data.byteLength : 0; console.info(`[Push] Received payload: ${ciphertextSize} bytes`); if (ciphertextSize > 4000) { console.warn('[Push] Payload close to 4 KB ceiling — verify server-side cap.'); } event.waitUntil( self.registration.showNotification('Notification', { body: event.data?.text() ?? '' }) ); }); -
Inspect raw HTTP responses from the push endpoint. In DevTools → Network, filter for POST requests to the FCM or autopush endpoint. Read
Content-Lengthon the request and the status code on the response. -
Cross-reference engine-specific response codes.
Engine Payload Too Large Network Abort Chrome 413 Request Entity Too LargeNetworkErrorFirefox 400 Bad Requestor201(silent)AbortErrorSafari 413 Request Entity Too LargeNetworkError -
Isolate with a minimal reproduction. Use the
web-pushCLI against a test subscription endpoint, stepping payload size up in 100-byte increments to find the exact rejection threshold per engine. Rawcurlrequests without correctaes128gcmframing will be rejected regardless of size — use the library.# Measure byte count of a candidate payload before passing to web-push echo -n '{"title":"Test","body":"Hello world"}' | wc -c
Enforcement Strategies
Pre-Encryption Validation (Mandatory)
Reject oversized payloads at the application layer before they reach the push service. This prevents quota waste and eliminates ambiguous 400 responses from Firefox.
const webpush = require('web-push');
webpush.setVapidDetails(
'mailto:ops@example.com',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
async function sendPushNotification(subscription, payload) {
const serialized = JSON.stringify(payload);
const byteLength = new TextEncoder().encode(serialized).length;
// Enforce 3,800-byte plaintext cap to account for aes128gcm overhead (≥38 bytes)
// plus web-push library padding. RFC 8030 hard limit is 4,096 bytes encrypted.
if (byteLength > 3800) {
throw new RangeError(
`Push payload too large: ${byteLength} bytes (max safe plaintext: 3,800 bytes)`
);
}
return webpush.sendNotification(subscription, serialized, { TTL: 86400 });
}
Always reference VAPID keys from environment variables (process.env.VAPID_PUBLIC_KEY / process.env.VAPID_PRIVATE_KEY) — never hardcode them. Refer to the VAPID Key Generation & Rotation guide for secure key storage patterns.
Payload Minimization (When Content Exceeds 3 KB)
For content that cannot be reduced below 3 KB — long article bodies, rich data structures — send only a notification ID and action type. Have the service worker or the opened page fetch the full payload after the notification is tapped.
// Server: send a minimal push — only the reference ID
const minimalPayload = {
notificationId: 'evt_abc123',
action: 'article_published'
};
// Service worker: fetch the real content on click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
fetch(`/api/notifications/${event.notification.data.notificationId}`)
.then(res => res.json())
.then(data => clients.openWindow(data.url))
);
});
This pattern also improves delivery analytics instrumentation because server-side content fetches provide a confirmed-display signal separate from the push delivery event.
CI/CD Pipeline Guard
Add a pre-flight byte-length check to your build pipeline. Fail the build if any notification template after JSON serialization exceeds 3,800 bytes.
// notification-size-check.js — run in CI before deployment
const templates = require('./notification-templates.json');
for (const [name, template] of Object.entries(templates)) {
const bytes = new TextEncoder().encode(JSON.stringify(template)).length;
if (bytes > 3800) {
console.error(`FAIL: template "${name}" is ${bytes} bytes — exceeds 3,800-byte safe limit`);
process.exit(1);
}
}
console.log('All notification templates within payload size limits.');
Gotchas & Edge Cases
- Firefox silent drops are invisible to the sender. The autopush service returns
201 Createdeven when it internally discards the message. Without client-sidebyteLengthtelemetry in the service worker, these failures are undetectable from the server side. - Unicode inflation. A payload that measures 3,800 characters can exceed 3,800 bytes if it contains multi-byte characters (emoji, CJK, Arabic). Always measure bytes with
TextEncoder, never.length. - Library padding variability. The
web-pushNode.js library adds random padding beyond the RFC 8291 minimum 38 bytes. The padding amount changes per send. The 3,800-byte plaintext cap provides a safe margin regardless of the padding amount chosen by the library on any given invocation. - 413 retries must reduce payload first. A
413response indicates the current payload exceeds capacity. Retrying without reducing payload size generates the same rejection, wastes push service quota, and may trigger rate limiting — see the retry logic and backoff strategies guide for correct handling. - VAPID key rotation does not affect payload size. A rotation in progress produces
401errors that can look like payload issues in aggregated error logs. Confirm the error code before assuming a size problem during a rotation window. aes128gcmis the only permitted content-encoding. The olderaesgcmencoding (RFC 8291’s predecessor) is not accepted by modern push services. Any library using legacy encoding will fail regardless of payload size.
Related
- Push API Payload Encryption — Full RFC 8291 encryption workflow: ECDH key exchange, HKDF derivation, and
aes128gcmrecord framing that creates the overhead budget described on this page. - aes128gcm Encoding & Decryption Errors — Diagnose decryption failures caused by malformed
aes128gcmrecords, padding errors, and key mismatches. - Cross-Browser Notification Quirks — Browser-specific delivery differences beyond payload size, including Firefox background throttling and Safari permission constraints.
- Retry Logic & Backoff Strategies — Correct retry behavior for
413and429responses, including payload reduction requirements before re-dispatch. - Delivery Analytics & Instrumentation — Correlate push delivery vs. display rates using client-side byteLength telemetry to detect silent drops.
Back to Push API Payload Encryption
FAQ
What is the actual maximum plaintext payload size for web push?
The RFC 8030 hard limit is 4,096 bytes of encrypted ciphertext. The aes128gcm framing envelope (RFC 8291) consumes at minimum 38 bytes, leaving roughly 4,058 bytes of ciphertext space. However, push libraries add variable-length padding on top of that. A 3,800-byte plaintext cap is the production-safe limit across Chrome, Firefox, and Safari — it absorbs both the mandatory framing overhead and any extra library padding.
Why does Chrome return 413 but Firefox return 201 for the same oversized payload?
Chrome routes web push through FCM (Firebase Cloud Messaging), which validates the Content-Length of the encrypted body server-side and returns 413 Request Entity Too Large immediately. Firefox uses Mozilla’s autopush service, which in some overflow scenarios accepts the POST with 201 Created but then internally discards the message before delivering it to the device. This is a push service implementation difference, not a browser bug. The only reliable safeguard is enforcing the 3,800-byte limit before dispatch, regardless of target browser.
Does compression help stay under the 4 KB limit?
HTTP content-encoding compression (gzip, brotli) applies to the HTTP transport layer, not to the encrypted push payload body. The push endpoint receives and validates the raw aes128gcm-encoded byte length. There is no standardized compression step inside the aes128gcm record format. The only effective strategies are payload minimization (removing non-essential fields) and the notification-ID-plus-fetch pattern described above.