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.

Web push 4 KB payload byte budget A horizontal bar showing the 4,096-byte ciphertext budget divided into: 38-byte aes128gcm framing, variable library padding, and 3,800-byte safe plaintext zone. 4,096-byte encrypted payload budget (RFC 8030 / RFC 8291) Safe plaintext ≤ 3,800 bytes Padding Frm 3,800 B recommended cap ~4,058 B math max 4,096 B hard limit Browser enforcement: Chrome 90+ 413 on overflow Firefox 90+ 400 or silent drop Safari (iOS 16.4+) 413 on overflow
Web push 4 KB payload byte budget: the 3,800-byte safe plaintext zone absorbs aes128gcm framing and library padding.

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.

  1. 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`);
    }
  2. Log ciphertext size in the service worker. Intercept the push event and record event.data.byteLength. If the value reported by byteLength after 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() ?? '' })
      );
    });
  3. Inspect raw HTTP responses from the push endpoint. In DevTools → Network, filter for POST requests to the FCM or autopush endpoint. Read Content-Length on the request and the status code on the response.

  4. Cross-reference engine-specific response codes.

    Engine Payload Too Large Network Abort
    Chrome 413 Request Entity Too Large NetworkError
    Firefox 400 Bad Request or 201 (silent) AbortError
    Safari 413 Request Entity Too Large NetworkError
  5. Isolate with a minimal reproduction. Use the web-push CLI against a test subscription endpoint, stepping payload size up in 100-byte increments to find the exact rejection threshold per engine. Raw curl requests without correct aes128gcm framing 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 Created even when it internally discards the message. Without client-side byteLength telemetry 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-push Node.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 413 response 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 401 errors that can look like payload issues in aggregated error logs. Confirm the error code before assuming a size problem during a rotation window.
  • aes128gcm is the only permitted content-encoding. The older aesgcm encoding (RFC 8291’s predecessor) is not accepted by modern push services. Any library using legacy encoding will fail regardless of payload size.

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.