TTL 0 vs TTL 86400: Choosing Between Drop-Now and Store-and-Forward Semantics
Every push dispatch carries an implicit contract about what happens when the target device is offline. TTL=0 enforces a hard discard — deliver this instant or throw it away — while TTL=86400 instructs the push service to queue the message for up to a day, forwarding it whenever the device reconnects. Choosing incorrectly produces either stale OTPs delivered hours late or live-score alerts buried silently on offline phones.
Quick-Answer Comparison
| TTL value | Push service behavior | Device offline | Best for | Risk |
|---|---|---|---|---|
0 |
Deliver immediately or discard; message is never stored | Message dropped silently; no 4xx/5xx error returned to sender | OTPs, 2FA codes, live scores, flash-sale triggers | Silent drop for any temporarily offline user; no retry possible |
60 |
Queue for up to 60 seconds, then discard | Dropped if device does not reconnect within the window | Short-lived alerts where 1-minute staleness is acceptable | Still drops users on spotty connections; narrow window for recovery |
3600 |
Queue for 1 hour | Delivered on reconnect within the hour | Order status updates, shipping notifications, appointment reminders | Marginally stale content; acceptable for most transactional use cases |
86400 |
Queue for 24 hours (store-and-forward) | Delivered on next reconnect within 24 hours | User-facing notifications, marketing, account alerts | Stale content risk; OTPs or time-limited links may have expired |
RFC 8030 §5.2 TTL Semantics
RFC 8030 §5.2 defines the TTL request header as a non-negative integer representing the maximum duration, in seconds, that the push service should retain the message. The semantics are explicit:
TTL: 0— the push service must attempt immediate delivery and must discard the message if the endpoint is not immediately reachable. The service returns a201 Createdto the sender regardless; silence on the other end is indistinguishable from immediate delivery.TTL: N(N > 0) — the push service may retain the message for up to N seconds. If the subscription endpoint becomes reachable within that window, the service delivers the message. If the window elapses before delivery, the message is silently discarded.
The RFC makes a deliberate design choice: the sender receives no delivery receipt. 201 Created confirms only that the push service accepted the message, not that it reached the browser. This is foundational to understanding why delivery tracking and acknowledgment must be implemented at the application layer, not assumed from HTTP response codes.
The TTL header value is bounded by the push service’s own maximum. RFC 8030 does not mandate a maximum; it only requires services to honor the value up to their internal limit. A push service may clamp higher values silently — the sender is not notified. The response TTL header echoes the value the service actually applied, which may be lower than what was requested. Always inspect the response TTL header in production dispatchers.
Offline Device Behavior
When a browser is closed, the device is powered off, or Android’s Doze mode suspends network activity, the push service receives no acknowledgment from the endpoint. Its handling depends entirely on the TTL the sender specified.
TTL=0 and offline devices. If the endpoint is unreachable at the moment of dispatch, the push service immediately discards the message. The sender receives 201 Created — the same response as a successful delivery. There is no error code, no webhook callback, and no retry. The message is gone. Applications relying on TTL=0 for security-critical alerts (OTP, account verification) must implement fallback channels: SMS, email, or in-app polling on next session.
TTL>0 and offline devices. The push service queues the encrypted payload. When the device reconnects and the service worker reestablishes its push channel, the service delivers the queued message. If multiple messages accumulated during the offline period, the service may deliver them in order, deliver only the most recent (collapse behavior), or throttle delivery based on internal rate limits.
Android Doze mode. Android 6+ Doze mode suspends network access for idle devices on battery. Even with TTL>0 and the device technically online, push delivery is deferred to maintenance windows (approximately every few hours). FCM uses high-priority messages ("priority": "high" in the FCM HTTP v1 API, paired with the Urgency: high Web Push header) to bypass Doze for critical alerts. Without the urgency signal, a message sent with TTL=86400 may sit undelivered for hours on a Doze-affected device even though the TTL has not expired and the device is powered on. See the backend delivery architecture overview for how Urgency interacts with FCM priority escalation.
Browser closed vs. tab closed. Service workers persist independently of browser tabs. A closed tab does not terminate the service worker’s push subscription. A closed browser, however, may terminate the service worker context entirely on some platforms (particularly on iOS with limited service worker support). On desktop Chrome and Firefox, the browser process typically runs headlessly in the background and receives push messages even when no windows are open.
FCM vs Mozilla Autopush TTL Handling
The two dominant web push services implement RFC 8030 TTL with meaningful behavioral differences.
FCM (Google). FCM stores messages for up to 28 days (2,419,200 seconds). Specifying a TTL value above this ceiling causes FCM to silently clamp to 28 days; the response TTL header reflects the clamped value. FCM supports collapse keys (Topic header in the Web Push protocol), which replace a queued message with a newer one sharing the same key. When a collapse key is active, a high-TTL message may be replaced before delivery — the effective delivery guarantee is weaker than the TTL alone implies. FCM also enforces per-device storage limits: if a device has 100 or more queued messages, FCM delivers a special collapse message indicating that messages were dropped, and the application must re-sync state on the next session.
Mozilla Autopush. Autopush enforces per-subscription storage limits rather than a global maximum TTL. By default, Autopush stores a limited number of messages per subscription channel (typically single digits). When the queue is full, older messages are evicted to make room for newer ones, regardless of their remaining TTL. This means a TTL=86400 message may be evicted before 86400 seconds elapse if the subscription receives a burst of messages while offline. Autopush also supports a crypto-key and encryption header legacy path alongside the RFC 8291 aes128gcm path; TTL behavior is consistent across both encoding schemes.
Practical implication. For applications targeting Firefox users, set TTL conservatively and do not assume store-and-forward guarantees equivalent to FCM. Implement in-payload timestamps (see the service worker snippet in the parent guide on TTL & Expiration Handling) so the service worker can detect and suppress stale messages that survived a long queue wait.
Choosing the Right TTL
TTL selection maps directly to the temporal relevance of the notification content.
Time-sensitive alerts (TTL 0–60). OTPs, 2FA codes, flash-sale countdown triggers, live scores, and payment confirmations. The information is valid for seconds or minutes at most. A TTL=0 ensures the user either gets the alert now or not at all — there is no risk of a 2FA code arriving 8 hours after it expired. Use TTL=60 as a pragmatic floor when a strict TTL=0 produces unacceptable drop rates on spotty mobile connections. Reference setting optimal TTL values for time-sensitive alerts for threshold calibration.
Transactional notifications (TTL 3600–86400). Order confirmations, shipping updates, password reset links with long validity windows, appointment reminders. Users expect these even if they come back online hours later. TTL=3600 is a safe default for same-day relevance. TTL=86400 covers a full offline day. Content must not contain time-limited tokens or links that expire before the TTL does.
Marketing and engagement (TTL 86400+). Re-engagement campaigns, weekly digests, feature announcements. The information remains valid for days. Use TTL=86400 as a baseline and consider up to 604800 (7 days) for low-urgency evergreen content. The primary risk is sending a “weekend sale ends tonight” message that arrives on a device that reconnects Monday morning. Always embed a human-readable expiration in the payload body and filter in the service worker.
The formula from the diagnostic guide applies here: TTL_optimal = relevance_window − p95_queue_latency − gateway_handshake_time. For TTL=0 decisions, the question is simpler: if the information is worthless to a user who receives it even 30 seconds late, TTL=0 is the correct choice.
TTL Interaction with Retry Logic and Batching
TTL and retry logic with backoff strategies operate at different layers but must be coordinated to avoid contradictory behavior.
Retry within TTL window. If a push dispatch returns a 5xx error or a network timeout, retrying makes sense only while the message’s TTL has not elapsed. A job enqueued at T=0 with TTL=300 and a first retry at T=250 leaves only 50 seconds of remaining validity. Cap retry attempts to max(0, TTL - elapsed_time) and set the backoff ceiling accordingly. Do not retry a TTL=0 message — by definition, the delivery window has already closed.
Batching and per-message TTL. When batching push dispatches for throughput, each message in the batch may carry a different TTL. A batch that takes 90 seconds to process will silently violate TTL=60 messages at the tail. Process time-sensitive low-TTL messages in a dedicated high-priority queue or dispatch lane, separate from TTL=86400 marketing batches. Never mix TTL=0 OTP messages into a batched marketing queue.
Queue-level TTL mirroring. The queue’s message retention must be at least as short as the push TTL. A Redis job key set with SETEX 86400 that sends a push with TTL: 3600 is safe. The reverse — a 3600-second Redis TTL on a job intended for 86400-second push delivery — causes the job to evict from the queue before the push service has finished its store-and-forward window, creating an unretriable inconsistency.
Code: TTL Selection by Notification Type
type NotificationType = 'otp' | 'transactional' | 'marketing';
/**
* Returns the recommended TTL in seconds for a given notification category.
* TTL=0 enforces deliver-or-drop semantics per RFC 8030 §5.2.
*/
function selectTTL(notificationType: NotificationType): number {
switch (notificationType) {
case 'otp':
// OTPs and 2FA codes: deliver immediately or discard.
// A code received minutes late is worse than no code — it causes
// user confusion and support overhead.
return 0;
case 'transactional':
// Order confirmations, shipping updates, account alerts.
// 1 hour covers same-session relevance; increase to 86400 for
// content valid across a full offline day.
return 3600;
case 'marketing':
// Re-engagement, feature announcements, promotional campaigns.
// 24 hours balances reach (offline users reconnect) against
// stale-content risk. Cap time-limited offers lower (e.g., 7200).
return 86400;
default:
// Exhaustiveness guard — TypeScript narrows `notificationType`
// to `never` here, so this branch is unreachable at compile time.
const _exhaustive: never = notificationType;
throw new Error(`Unknown notification type: ${_exhaustive}`);
}
}
export { selectTTL };
POST https://fcm.googleapis.com/fcm/send/APA91bHPRgkFLiiQvWT5NlkYwVKpqMoF3RdoKEZHk HTTP/1.1
Authorization: WebPush eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/octet-stream
Content-Encoding: aes128gcm
TTL: 0
Urgency: high
Topic: otp-12345
<encrypted-binary-payload>
// Node.js — web-push library with explicit TTL per notification type
import webpush from 'web-push';
import { selectTTL } from './ttl-selector.js';
webpush.setVapidDetails(
'mailto:ops@example.com',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY,
);
async function sendPush(subscription, payload, notificationType) {
const ttl = selectTTL(notificationType);
const options = {
TTL: ttl,
urgency: notificationType === 'otp' ? 'high' : 'normal',
// topic collapses multiple messages with the same key at the push service
topic: notificationType === 'otp' ? `otp-${payload.userId}` : undefined,
};
try {
const response = await webpush.sendNotification(
subscription,
JSON.stringify({ ...payload, sentAt: Date.now(), ttl }),
options,
);
// Inspect the echoed TTL — push service may have clamped it
const appliedTtl = response.headers['ttl'];
if (appliedTtl && Number(appliedTtl) < ttl) {
console.warn(`TTL clamped by push service: requested ${ttl}, applied ${appliedTtl}`);
}
return response;
} catch (err) {
if (err.statusCode === 410) {
// Subscription has expired — remove from database
throw new Error(`Endpoint gone (410): ${err.endpoint}`);
}
throw err;
}
}
Notification Type TTL Reference
| Notification type | Recommended TTL | Reasoning | Risk if TTL too high | Risk if TTL too low |
|---|---|---|---|---|
| OTP / 2FA code | 0 |
Code is valid for 30–300 s; a late delivery is worse than no delivery | Expired code delivered; user confusion, support tickets | N/A — TTL=0 is already the floor |
| Flash sale / countdown | 0–60 |
Offer window is minutes long; stale trigger causes invalid redemption | User clicks a dead offer; cart errors, chargeback risk | Silent drop for offline users; missed revenue |
| Live score / sports alert | 0–120 |
Score is outdated within minutes; stale data confuses the user | Out-of-date score delivered; user sees wrong state | Offline users miss the event entirely |
| Order confirmation | 3600 |
Content is valid for hours; users check within the same session | No meaningful downside within a day | User misses confirmation after going offline post-purchase |
| Shipping / delivery update | 86400 |
Device may be offline for hours; full-day store-and-forward is safe | Marginally stale tracking link; usually acceptable | Dropped update causes unnecessary support contacts |
| Marketing / promotional | 86400 |
Broad audience; many users offline during send window | Time-limited offers (e.g., “tonight only”) delivered the next day | Drop rate increases; undermines campaign reach |
| Security alert (breach) | 3600 |
Urgency is high but content remains actionable for hours | Acceptable if the incident is still active | Critical security guidance dropped for offline users |
SVG Diagram: TTL=0 vs TTL=86400 Delivery Timelines
Gotchas and Edge Cases
-
TTL=0 silently drops for offline users — no error is returned. The push service responds
201 Createdin both cases: immediate delivery and immediate discard. The sender has no way to distinguish the two outcomes without application-level acknowledgment. If your OTP delivery SLA requires knowing whether the message was received, you need a separate in-app callback or a polling mechanism. -
FCM collapse keys interact with TTL non-obviously. When you set a
Topicheader (FCM collapse key) on a TTL>0 message, FCM replaces any previously stored message with the same key. A user offline for 20 minutes who received 10 messages with the same collapse key and TTL=3600 will receive exactly one message on reconnect — the most recent. If the most recent message is a stale replacement of a more important earlier message, the important content is lost permanently. -
Android Doze mode defers delivery even when TTL>0. A device in Doze maintains its push subscription and the push service retains the message within the TTL window. However, the operating system does not wake the service worker until the next Doze maintenance window. For messages with TTL=60, the device may never exit Doze before the TTL elapses. Use
Urgency: highto request FCM priority escalation, which bypasses Doze for critical messages. Verify this is not abused — FCM rate-limits high-urgency messages per app. -
Push services silently clamp TTL values. Sending
TTL: 9999999to FCM results in the service applying its 28-day (2,419,200-second) maximum. The responseTTLheader contains the clamped value. Always read the response header rather than assuming the requested TTL was honored. Autopush enforces lower limits and may clamp more aggressively than FCM. -
VAPID
expclaim and message TTL are independent values. The VAPID JWT’sexpfield governs how long the VAPID authentication token is valid — it does not affect message storage at the push service. A common mistake is settingexpto match the message TTL, which produces401 Unauthorizederrors when short-lived VAPID tokens expire before the push service processes the request under load. Set VAPIDexptonow + 12h(or the maximum 24h) regardless of the message TTL. The two values serve entirely different purposes.
Notification Type TTL Recommendations (Extended)
Refer to the setting optimal TTL values guide for per-category threshold derivation and queue latency formulas.
FAQ
What HTTP response do I get when TTL=0 causes a message to be dropped?
The push service returns 201 Created in both cases — successful delivery and immediate discard. RFC 8030 does not define a distinct status code for TTL=0 drops. If the device endpoint was reachable, the message was delivered; if not, it was silently discarded. You cannot determine which occurred from the HTTP response alone. Implement application-level acknowledgment (a beacon from the service worker back to your server on push receipt) to distinguish delivery from discard.
Can I update a queued message to extend its TTL after dispatch?
No. RFC 8030 does not define a mechanism to modify a queued message’s TTL after the initial POST to the push service. The only way to replace a queued message is to send a new message with the same Topic collapse key (FCM) or the same Topic header on Autopush-compatible services. The new message overwrites the queued one with the new TTL and payload. You cannot extend the TTL of the original message in place.
Does TTL affect the Urgency header's behavior?
TTL and Urgency are orthogonal. Urgency influences delivery priority within a battery or network budget: a high-urgency message may wake a Doze-suspended device or bypass low-power delivery deferral. TTL governs how long the push service stores the message before discarding it. A message with Urgency: high and TTL: 0 is not stored at all — urgency has nothing to queue. A message with Urgency: low and TTL: 86400 is stored for 24 hours but delivered at the push service’s discretion during low-power periods. Combine both headers intentionally: critical alerts need TTL: 0 and Urgency: high; background syncs benefit from TTL: 86400 and Urgency: very-low.
Related
- Setting Optimal TTL Values for Time-Sensitive Alerts — threshold derivation formulas, queue latency measurement, and per-category calibration.
- Retry Logic & Backoff Strategies — how to cap retry attempts within the TTL window and avoid wasted dispatches on expired messages.
- Delivery Tracking & Acknowledgment — application-layer receipt confirmation to compensate for TTL=0’s silent-drop behavior.
- Backend Delivery Architecture & Queue Management — system-level context for integrating TTL policy with queue design, batching, and throughput optimization.
Back to TTL & Expiration Handling