TTL & Expiration Handling in Web Push

Time-To-Live (TTL) dictates the maximum duration a push message remains queued in the browser vendor’s push service before automatic expiration. Proper configuration prevents stale alerts, reduces unnecessary device wake-ups, and optimizes queue resources within your broader Backend Delivery Architecture & Queue Management. This guide establishes the architectural boundary between transport-layer expiration and application-level lifecycle management, providing production-ready patterns for header injection, client-side filtering, and secure queue enforcement.

Understanding TTL in the Web Push Lifecycle

TTL operates at the transport layer, independent of your application’s business logic. When a push message is dispatched, the vendor push service (FCM, APNs, or Mozilla) stores the encrypted payload until the target endpoint reconnects or the TTL window elapses. Misconfigured TTL values directly impact infrastructure costs, battery consumption, and user trust.

Implementation Directives:

  • Categorize TTL by Notification Type: Assign strict TTL windows based on payload criticality. Transactional alerts (e.g., OTPs, payment confirmations) require TTL: 0–300. Promotional campaigns tolerate TTL: 86400–172800. System health alerts should default to TTL: 3600.
  • Map Vendor Defaults to SLAs: FCM defaults to 4 weeks, APNs to 24 hours, and Mozilla to 24 hours. Never rely on vendor defaults; always override explicitly to guarantee deterministic expiration.
  • Model TTL Decay for Engagement Analytics: Track delivery latency vs. TTL expiration to build churn prediction curves. Messages delivered at >80% of their TTL window typically yield <5% open rates.

Security & Compliance Note: TTL directly impacts data retention windows under GDPR/CCPA. Shorter TTLs reduce the attack surface for unauthorized data exposure in vendor queues. Treat the push service as an untrusted intermediary; assume any payload exceeding its TTL is cryptographically inaccessible but metadata may persist in vendor logs.

Implementing TTL in Push Payloads & HTTP Headers

Web push relies on the TTL HTTP header (RFC 8030) to communicate expiration to the push service. Implementing this correctly requires precise header injection, strict validation, and payload structuring. For time-sensitive campaigns, refer to our guide on Setting optimal TTL values for time-sensitive alerts.

Production HTTP Dispatch Pattern:

POST https://fcm.googleapis.com/fcm/send/<subscription_endpoint> HTTP/1.1
Authorization: Bearer <vapid_jwt>
Content-Type: application/octet-stream
Content-Encoding: aes128gcm
TTL: 3600
Urgency: high
Crypto-Key: <public_key>
Encryption: <salt>

<encrypted_binary_payload>

Implementation Directives:

  • Enforce Explicit TTL Headers: Inject TTL (in seconds) on every dispatch call. Reject requests lacking this header at the API gateway level.
  • Coordinate with Urgency: Pair TTL with the urgency header (very-high, high, normal, low). High urgency does not override TTL; it only influences vendor delivery prioritization within the window.
  • Validate Against Vendor Limits: Clamp TTL values to 0–2419200 seconds (4 weeks). Reject out-of-bounds values with 400 Bad Request before queue ingestion.

Security & Compliance Note: Never embed PII, session tokens, or routing metadata in the TTL header. Headers must remain strictly numeric and validated server-side to prevent header injection or parser exploitation.

Service Worker Expiration Handling & Stale Message Filtering

When a device reconnects after extended offline periods, queued messages may arrive past their useful window. Implement expiration checks directly in the push event listener using payload metadata and device clock synchronization. Integrate this logic with your Delivery Tracking & Acknowledgment pipeline to suppress expired notifications before rendering.

Production Service Worker Implementation:

self.addEventListener('push', (event) => {
 // Decrypt payload in production using event.data.arrayBuffer() and Web Crypto API
 // This example assumes decrypted JSON for TTL demonstration purposes
 const payload = event.data ? event.data.json() : {};
 const maxAgeSec = payload.ttl ?? 0;
 const sentAtMs = payload.timestamp ?? Date.now();
 const nowMs = Date.now();
 
 // Allow ±5s clock skew tolerance for offline devices
 const CLOCK_SKEW_MS = 5000;
 const isExpired = maxAgeSec > 0 && (nowMs - sentAtMs) > (maxAgeSec * 1000) + CLOCK_SKEW_MS;

 if (isExpired) {
 // Suppress rendering; do not log payload contents
 console.debug(`[Push TTL] Expired message suppressed (age: ${Math.round((nowMs - sentAtMs)/1000)}s)`);
 event.waitUntil(Promise.resolve());
 return;
 }

 event.waitUntil(
 self.registration.showNotification(payload.title, {
 body: payload.body,
 tag: payload.tag || 'default',
 badge: '/icons/badge.png',
 data: { 
 expiresAt: sentAtMs + (maxAgeSec * 1000),
 trackingId: payload.id 
 }
 })
 );
});

Implementation Directives:

  • Embed Metadata in Encrypted Payload: Always include timestamp (epoch ms) and ttl (seconds) inside the encrypted JSON body. The push service cannot read these fields.
  • Apply Clock-Skew Tolerance: Offline devices often drift. Use a ±5s buffer before marking a payload expired to prevent false negatives.
  • Fail Fast on Expiration: Return early from the push event to conserve battery, prevent UI flicker, and avoid unnecessary network calls to your tracking endpoints.

Security & Compliance Note: Client-side filtering must never log expired payloads to localStorage, IndexedDB, or analytics beacons. Use console.debug exclusively for development. Expired payloads are considered cryptographically stale and must be discarded immediately.

Backend Queue TTL Enforcement & Cleanup Strategies

Message brokers must enforce TTL before dispatch to prevent unnecessary push service load. Configure queue-level expiration policies and dead-letter routing for expired jobs. When coordinating with Message Batching & Throughput Optimization, ensure TTL is evaluated per-batch to avoid partial delivery of stale alerts.

Production Redis Queue Pattern (Python):

import time
import json
import logging
from redis import Redis, ConnectionPool

logger = logging.getLogger(__name__)
pool = ConnectionPool(host='redis.internal', port=6379, db=0, max_connections=50)
redis = Redis(connection_pool=pool)

def enqueue_push(job_id: str, payload: dict, ttl_seconds: int) -> None:
 """Atomically enqueue a push job with strict TTL enforcement."""
 if not (0 <= ttl_seconds <= 2419200):
 raise ValueError("TTL must be between 0 and 2419200 seconds")
 
 queue_key = f"push:queue:{job_id}"
 dispatch_key = "push:dispatch_list"
 
 # setex guarantees automatic eviction at TTL boundary
 redis.setex(queue_key, ttl_seconds, json.dumps(payload))
 redis.lpush(dispatch_key, job_id)

def validate_and_dispatch(job_id: str) -> bool:
 """Atomic pre-dispatch check. Returns False if expired."""
 pipe = redis.pipeline()
 try:
 pipe.exists(f"push:queue:{job_id}")
 pipe.get(f"push:queue:{job_id}")
 exists, raw_payload = pipe.execute()
 
 if not exists:
 logger.info(f"Job {job_id} expired in queue. Skipping dispatch.")
 # Route to dead-letter queue for analytics
 redis.lpush("push:dlq:expired", json.dumps({"job_id": job_id, "reason": "ttl_expired"}))
 return False
 
 payload = json.loads(raw_payload)
 # Proceed with HTTP push dispatch
 return True
 except Exception as e:
 logger.error(f"Dispatch validation failed for {job_id}: {e}")
 return False

Implementation Directives:

  • Mirror TTL Across Layers: Set Redis/Kafka/RabbitMQ message TTL to exactly match the intended HTTP TTL header. Mismatches cause phantom deliveries or premature drops.
  • Implement Pre-Dispatch Validation: Use atomic EXISTS + GET checks before invoking vendor APIs. Drop expired jobs immediately to preserve throughput.
  • Route to Dead-Letter Queues (DLQ): Capture expired job metadata in a dedicated DLQ. Use this data for churn modeling and campaign effectiveness analysis, not for re-delivery.

Security & Compliance Note: Queue-level TTL must align with data minimization principles. Configure brokers to auto-delete payloads immediately upon expiration. Never retain raw push payloads in persistent logs beyond their TTL window.

Compliance Alignment & Secure TTL Practices

Expired push messages containing PII or promotional offers can violate GDPR/CCPA data minimization principles if cached or logged improperly. Enforce strict TTL boundaries, encrypt payloads end-to-end, and purge delivery logs post-expiration. Implement audit trails for TTL overrides to maintain compliance during security reviews.

Implementation Directives:

  • Zero-Trust Log Rotation: Configure log aggregation pipelines (e.g., Vector, Fluentd) to scrub push payload contents after TTL + 24h. Retain only anonymized delivery status codes and timestamps.
  • VAPID Encryption Enforcement: Never dispatch unencrypted payloads. VAPID ensures only the target service worker can decrypt the message, preventing intermediary TTL manipulation or payload inspection.
  • Audit TTL Overrides: Restrict TTL modification to authorized service accounts. Log all programmatic TTL changes with actor_id, previous_value, new_value, and timestamp for regulatory audits.
  • Disable Custom Bypass Endpoints: Remove any internal APIs that allow TTL: -1 or infinite expiration. Hardcode maximum TTL caps at the infrastructure level.

Security & Compliance Note: Maintain cryptographic proof of TTL enforcement for regulatory audits. Avoid custom TTL bypass endpoints. Treat the expiration window as a hard security boundary, not a soft recommendation.

Testing TTL Behavior & Edge Cases

Validate TTL expiration across network conditions, browser states, and vendor throttling. Use browser developer tools to simulate offline periods and verify service worker filtering. Test TTL: 0 for immediate delivery and TTL: 1 for rapid expiration scenarios to ensure graceful degradation.

Implementation Directives:

  • Inspect Queued State: Navigate to chrome://serviceworker-internals or Firefox about:debugging to monitor pending push events and verify expiration timestamps.
  • Simulate Network Transitions: Use DevTools Network throttling (OfflineFast 3GOnline) to trigger delayed delivery. Verify that expired payloads are suppressed without console errors.
  • Validate Boundary Conditions:
  • TTL: 0: Should bypass vendor queueing entirely. Verify immediate delivery or 410 Gone if endpoint is unreachable.
  • TTL: 1: Should expire rapidly. Confirm service worker drops the message if processing latency exceeds 1 second.
  • Monitor Vendor Responses: Track HTTP 410 (Gone) and HTTP 429 (Too Many Requests) in your dispatch logs. These indicate expired endpoints or vendor throttling, not TTL misconfiguration.

Security & Compliance Note: All TTL validation must occur in isolated staging environments. Never inject production PII or real subscription endpoints into offline simulation tests. Use synthetic payloads and mock VAPID keys exclusively.