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.
Prerequisites
pushlistener you control for client-side filteringtimestampandttlfields embedded inside the encryptedaes128gcmpayload
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, Mozilla Autopush, or APNs Web Push) 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. OTPs and payment confirmations:
TTL: 0–300s. Promotional campaigns:TTL: 86400–172800s. System health alerts:TTL: 3600s. - Understand Vendor Defaults — and Override Them: The Web Push Protocol (RFC 8030) requires clients to supply a
TTLheader; it has no mandatory default. FCM uses 4 weeks ifTTLis omitted. APNs Web Push uses theapns-expirationUnix timestamp header (not a duration), defaulting to0which means “deliver immediately or discard”. Always setTTLexplicitly for deterministic behavior. - 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. 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: WebPush <vapid_jwt>
Content-Type: application/octet-stream
Content-Encoding: aes128gcm
TTL: 3600
Urgency: high
<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-low,low,normal,high). High urgency influences vendor delivery prioritization within the TTL window but does not extend it. - Validate Against Provider Limits: The Web Push Protocol (RFC 8030) permits
TTLvalues from0to2419200seconds (28 days). Clamp values to this range. APNs Web Push acceptsapns-expirationas a Unix epoch timestamp;0means “discard if not immediately deliverable”. The behavioral gap between the two extremes — fire-and-forget versus day-long buffering — is unpacked in TTL 0 vs TTL 86400 delivery guarantees.
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 an extended offline period, queued messages may arrive past their useful window. Implement expiration checks directly in the push event listener using payload metadata. Integrate this logic with your Delivery Tracking & Acknowledgment pipeline to suppress expired notifications before rendering.
Production Service Worker Implementation:
self.addEventListener('push', (event) => {
// The browser decrypts the aes128gcm payload automatically before delivering
// it to the service worker. event.data.json() returns the decrypted JSON.
const payload = event.data ? event.data.json() : {};
const maxAgeSec = payload.ttl ?? 0;
const sentAtMs = payload.timestamp ?? Date.now();
const nowMs = Date.now();
// Allow ±5 s clock skew tolerance for offline devices
const CLOCK_SKEW_MS = 5000;
const isExpired = maxAgeSec > 0
&& (nowMs - sentAtMs) > (maxAgeSec * 1000) + CLOCK_SKEW_MS;
if (isExpired) {
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 ±5 s 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 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-job 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 (RFC 8030)")
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.")
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 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 for churn modeling and campaign effectiveness analysis, not for re-delivery. The same window must bound your retry pipeline — a retry scheduled past the original TTL is wasted compute.
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.
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. RFC 8291 end-to-end encryption ensures only the target service worker can decrypt the message.
- 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 Infinite Expiration: Remove any internal APIs that allow
TTL: -1or unbounded expiration. Hardcode maximum TTL caps at the infrastructure level.
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.
Implementation Directives:
- Inspect Queued State: Navigate to
chrome://serviceworker-internalsor Firefoxabout:debugging#/runtime/this-firefoxto 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 or discard if the endpoint is unreachable. The push service does not queue it.- Short TTL (e.g.,
TTL: 5): Confirm the service worker drops the message if processing latency causes the payload to arrive stale.
- 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. Route them to Delivery Tracking & Acknowledgment rather than the TTL purge path.
TTL Configuration Reference
Set TTL by notification class, and mirror that value across every layer so the broker, the push service, and the service worker agree on the same window.
| Parameter | Type | Default | Notes |
|---|---|---|---|
ttlSeconds (RFC 8030 TTL header) |
integer | per-tier | 0–2419200; clamp at the API gateway |
urgency |
enum | normal |
very-low | low | normal | high; affects prioritization, not the window |
brokerTtlSeconds |
integer | = ttlSeconds |
Redis setex / RabbitMQ message TTL; must match the header |
clockSkewMs |
integer | 5000 |
Tolerance applied in the service worker before marking a payload stale |
payload.timestamp |
epoch ms | dispatch time | Embedded in encrypted body for client-side age checks |
expiredDlqRetention |
integer | 604800 |
How long expired-job metadata is kept for churn analysis |
Suggested TTL tiers: OTP / payment confirmation 0–300 s, system health alert 3600 s, promotional campaign 86400–172800 s. The reasoning behind time-sensitive choices is in Setting optimal TTL values for time-sensitive alerts.
Related
- Setting optimal TTL values for time-sensitive alerts — choosing the window for OTPs, alerts, and campaigns.
- TTL 0 vs TTL 86400 delivery guarantees — what each extreme actually promises about delivery.
- Retry Logic & Backoff Strategies — keep every retry inside the TTL window.
- Message Batching & Throughput Optimization — evaluate TTL per job so batches never carry stale alerts.
- Push API Payload Encryption — the
aes128gcmbody where thetimestampandttlfields live.
Back to Backend Delivery Architecture & Queue Management
FAQ
What is the maximum TTL for a web push message?
The Web Push Protocol (RFC 8030) permits a TTL header from 0 to 2419200 seconds (28 days). FCM defaults to 4 weeks if the header is omitted, but you should always set it explicitly. APNs Web Push instead uses the apns-expiration Unix timestamp.
What does TTL: 0 actually do?
TTL: 0 tells the push service to deliver the message only if the endpoint is reachable right now; if the device is offline, the message is discarded rather than queued. It is fire-and-forget delivery, contrasted with longer windows in TTL 0 vs TTL 86400 delivery guarantees.
Why filter expired messages in the service worker if the push service already expires them?
Because a device that reconnects near the end of the TTL window can still receive a message that is no longer useful to the user. A freshness check in the push listener — using the embedded timestamp and ttl plus a small clock-skew tolerance — suppresses those late arrivals before they render.
Should broker TTL match the HTTP TTL header?
Yes. Mismatches cause phantom deliveries (broker keeps a job the service would have dropped) or premature drops (broker evicts a job that still had a valid window). Set the Redis/RabbitMQ/Kafka message TTL equal to the intended TTL header value.
Does higher Urgency extend the TTL?
No. Urgency (very-low through high) only influences how the push service prioritizes delivery within the existing window. It never lengthens the TTL. Use the TTL header alone to control lifetime.