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 tolerateTTL: 86400–172800. System health alerts should default toTTL: 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: PairTTLwith theurgencyheader (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–2419200seconds (4 weeks). Reject out-of-bounds values with400 Bad Requestbefore 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) andttl(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
pushevent 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
TTLheader. Mismatches cause phantom deliveries or premature drops. - Implement Pre-Dispatch Validation: Use atomic
EXISTS+GETchecks 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, andtimestampfor regulatory audits. - Disable Custom Bypass Endpoints: Remove any internal APIs that allow
TTL: -1or 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-internalsor Firefoxabout:debuggingto monitor pending push events and verify expiration timestamps. - Simulate Network Transitions: Use DevTools Network throttling (
Offline→Fast 3G→Online) 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 or410 Goneif 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)andHTTP 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.