Streak Reminder Push Notifications: Prevention First

Author
Charlie Hopkins-BrinicombeCharlie Hopkins-Brinicombe

Across Trophy's platform, only 0.9% of users return to start a new streak after losing a 2-3 day streak. That number climbs to 9.1% at 31-60 days and that trend dictates where push notification effort actually pays off.

This post covers why recovery-focused push strategies underperform, how Trophy's push primitives handle the parts that break when teams build them from scratch, and the decisions behind the defaults.

Bar chart of streak return rates by length of lost streak across Trophy's platform. Users who lose a 31-60 day streak return at 9.09%, compared to 0.9% for users who lose a 2-3 day streak.
Length of streak that was lost (days) Return rate (%)
2-3 0.90
4-7 1.42
8-14 1.54
15-30 2.49
31-60 9.09

Source: Trophy platform data, April 2026. Return rate is the percentage of users who started a new streak at any point after losing their previous one, segmented by the length of the streak they lost.

The actionable read of this table is that users who lose short streaks are, in practice, lost users. Recovery notifications sent to this cohort are an expensive way to move the 0.9% number slightly. Users who have invested more than 30 days in a streak are a different story: the return rate rises 3x between the 15-30 and 31-60 buckets. The operational implication is that push effort compounds most when it prevents loss on high-streak users, not when it tries to recover it on low-streak ones.

The naive approach

The default path when building streak reminder push notifications is to wire up Firebase Admin SDK (or direct APNs) behind a cron job that queries for users whose streak is about to expire.

Most teams don't have the time-series user interaction data required to calculate streaks properly, but for those who do the code would look something like this:

import admin from 'firebase-admin';

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});

// Runs on a cron at 20:00 UTC
const atRiskUsers = await db.query(`
  SELECT u.id, u.device_tokens, s.current_streak_length
  FROM users u
  JOIN streaks s ON s.user_id = u.id
  WHERE s.current_streak_length > 0
    AND s.last_extended_at < NOW() - INTERVAL '20 hours'
`);

for (const user of atRiskUsers) {
  for (const token of user.device_tokens) {
    await admin.messaging().send({
      token,
      notification: {
        title: "Don't lose your streak!",
        body: `You're on a ${user.current_streak_length}-day streak. Open the app to keep it going.`,
      },
    });
  }
}

This code handles FCM only. For iOS you either add a separate APNs integration with its own SDK, auth flow, and error handling, or you standardise on Expo Push Service and accept its abstraction constraints. Either path starts a second maintenance stream.

The same shape of problem applies here as it did for streak reminder emails: a cron-plus-query-plus-send pipeline looks tractable on day one and then accumulates edge cases. The difference is that push is a more interruptive surface than email, which means the penalty for sending the wrong message at the wrong moment is proportionally higher. Users who lose trust in your push notifications disable them, and once they're disabled you have no way to earn that channel back.

Why the naive approach breaks in production

The failure modes cluster into five categories.

Platform fragmentation between APNs and FCM. iOS and Android use different services with different auth models, different error codes, and different rate-limit behaviours. Every feature you build is two implementations in parallel: token management, batch sends, delivery retries, content formatting. Expo Push Service abstracts most of this, but only if you standardise on it at the start.

Device token lifecycle. Push tokens rotate when users reinstall the app, switch phones, or in some cases on OS updates. If your server doesn't listen for Unregistered (APNs) or UNREGISTERED (FCM) errors and clean up stale tokens, you keep sending to dead addresses indefinitely. Dead addresses waste quota and count against your sender reputation on some platforms.

Race conditions around extension. A user opens the app at 7:59 PM local time, extends their streak, and closes the app. The cron job runs at 8:00 PM UTC, finds them as at-risk (the read replica was stale), and fires a push that says "don't lose your streak." On a mobile surface, that reads as the app being confused about what the user just did. This is exactly the kind of error that makes users turn push off for your app.

Rate limiting and batching. APNs and FCM both impose rate limits on bulk sends. Handling batch endpoints, back-off on 429 responses, and retry queues correctly is its own engineering track, and getting it wrong means silent delivery failures that you only notice through low engagement.

Content staleness. Push notification payloads are prepared at send time but delivered with some latency. A message that said "you have 3 hours left" can arrive when the user has 2 hours left, or none. For notifications that reference freeze state or progress toward an achievement, the window between payload construction and user-visible delivery becomes a correctness problem rather than a cosmetic one.

Each of these is solvable. The question is whether the team building the core product should be spending its time on them.

The Trophy approach

Trophy's push notifications are a built-in type, configured through the dashboard and dispatched based on the user state Trophy already tracks. Sending streak reminder pushes reduces to three pieces of work: pick a channel, identify users with their device tokens, and activate the streak template.

On the channel side, Trophy supports Apple Push Notification Service, Firebase Cloud Messaging, and Expo Push Service. For apps without an existing investment in APNs or FCM, Expo is the lowest-friction choice because it handles platform fragmentation automatically and means you don't maintain two sets of credentials. More detail on the full email and push feature set is on the features page.

Device tokens get associated with each user the same way timezone does, inline on metric events or via explicit identify:

import { TrophyApiClient } from "@trophy/sdk-node";

const trophy = new TrophyApiClient({ apiKey: process.env.TROPHY_API_KEY });

await trophy.metrics.event("flashcards-flipped", {
  user: {
    id: user.id,
    email: user.email,
    tz: user.timezone,
    deviceTokens: user.deviceTokens, // array of push tokens across devices
  },
  value: 1,
});

The client-side token capture depends on your stack. For Expo, it's a single call:

import * as Notifications from 'expo-notifications';

const { data: token } = await Notifications.getExpoPushTokenAsync();

For native iOS or Android, you capture the token through your own APNs or FCM integration and pass it into Trophy through the identify call above. Either way, the Trophy user record now knows where to send.

User preferences are managed through a dedicated preferences API rather than a single opt-out flag. This matters because "I want streak reminders by email but not push" is a common preference that a binary opt-out can't express:

await trophy.users.updatePreferences("user-123", {
  notifications: {
    streak_reminder: ["push"],                // push only
    achievement_completed: ["email", "push"], // both
    recap: ["email"],                         // email only
    reactivation: [],                         // disabled
  },
});

Activate the streak template in the dashboard and the dispatch logic Trophy has already built takes over: timezone-aware scheduling, state checking at send time, and device-level coordination are all handled server-side.

Trophy dashboard push notification builder showing the streak reminder template with user state variables and channel configuration.
Streak reminder push notification templates in Trophy

The decisions behind the default

A few defaults encode opinions about what makes streak reminder pushes work, and they're worth explaining.

Streak pushes fire with reference to the user's local day-end, not UTC. The tz associated with each user drives every scheduling decision, including when "approaching end of day" actually means. A user who moves timezones gets correct reminders as soon as the tz is updated on the next identify call.

Push content is aware of freeze state. Users who have freezes available average longer streaks: 17.19 days on daily streaks with freezes, compared to 11.62 days without. That population is exactly the high-value cohort where push effort compounds, which means telling them to panic about a streak that a freeze will automatically protect is the fastest way to teach them your notifications don't know what's going on. Trophy's template system exposes freeze state, so the reminder can adjust tone accordingly.

Reactivation is a separate notification type from streak reminders. Once a streak is lost, the question isn't "extend your streak" but "come back and start a new one," and the return rate data above argues for sending that message selectively. Trophy's reactivation notifications are a distinct type with their own template, which means you can enable them for your long-streak cohort without firing them indiscriminately. The broader pattern of what happens when users lose streaks covers the full reactivation design space.

Timing draws on the same weekday pattern as email. We covered this in our streak reminder emails post: Friday is the peak day for daily streak loss across Trophy's platform, with a share roughly twice what random distribution would predict. Push notifications face the same distribution but with tighter delivery constraints, since pushes are more time-sensitive than email. The practical effect is that the late-afternoon window on Friday is where the highest-leverage streak pushes land.

FAQ

Do streak reminder pushes fire for users who don't have freezes available? Yes. The send decision is based on streak state, specifically whether the streak is at risk of expiring in the user's local timezone, not on freeze availability. Freezes change the copy and tone of the push rather than whether it fires. For users with freezes, the reminder can position the freeze as a backup; for users without, the reminder is more direct about the immediate risk.

Can I send push notifications for achievement completions as well as streaks? Yes. Trophy supports four notification types: achievement_completed, recap, reactivation, and streak_reminder. Each has its own template and its own per-channel preference. You can enable all four, or any subset, per user via the preferences API.

What happens if a user has multiple devices? Device tokens are stored as an array on the user record. A push notification for that user is dispatched across the associated tokens, so the user sees the notification on each device they've enabled push on. When a token expires or is rejected by APNs or FCM, Trophy handles the error response and removes the stale token from the user record.

Which channel should I use if I'm starting from scratch? For cross-platform apps built in Expo or React Native, use Expo Push Service. It's one integration instead of two and handles the APNs and FCM routing automatically. For native iOS or Android apps with existing push infrastructure, use APNs and FCM directly. The integration work is similar to what you've already done for other notification types, and Trophy supports both channels natively.

How do I let users opt in to push reminders during onboarding? The update preferences API accepts an array of channels per notification type, so enabling push specifically on streak reminders is one call. Combined with the OS-level permission prompt from your client SDK, this lets you build a preference flow where users see exactly what they're opting into and can toggle individual types rather than a single on/off switch.

What about users who signed up through the web and don't have device tokens? Users without device tokens won't receive pushes. There's nowhere to send them. For these users, Trophy falls back to email if email is enabled on the notification type in their preferences. This is why most apps configure streak_reminder with both email and push as default channels: the channel the user actually receives depends on what they've granted.

Conclusion

The return rate data at the top of this post is the short version of a longer argument: push notifications are leverage, and leverage compounds on users who are already engaged. A streak reminder sent to a user on a 3-day streak recovers the streak 0.9% of the time. The same reminder sent to a user on a 60-day streak recovers it at closer to 100%. Treating those two cohorts with the same urgency, copy, and cadence is the single most common push strategy failure in gamified apps, and the one with the clearest data correction.

For the full set of Trophy's push notification configuration options, the push notifications platform documentation covers every setting referenced here.


Author
Charlie Hopkins-BrinicombeCharlie Hopkins-Brinicombe

Get the latest on gamification

Product updates, best practices, and insights on retention and engagement — delivered straight to your inbox.

Streak Reminder Push Notifications: Prevention First - Trophy