Skip to main content

Guide

How to Implement Real-Time Notifications

Production real-time notifications -- WebSockets vs polling, notification channels, in-app and push delivery, user preferences, and the patterns that scale without overwhelming users.

Category Guide
Read Time 11 min read
Updated April 2026
Steps 5 steps

Who This Guide Is For

This guide is for developers building notification systems for web and mobile applications. You want to deliver timely updates to users — new messages, status changes, system alerts — without requiring them to refresh the page or poll manually. You understand HTTP basics and want to implement real-time delivery that works reliably in production, handles multiple channels (in-app, push, email), and respects user preferences rather than becoming a source of noise.

Before You Start

You should have a working application with user authentication and at least one event that users need to know about in near-real time. This guide covers the delivery infrastructure, not the business logic of what to notify about — that is a product decision. Technically, you should be comfortable with asynchronous processing (queues and workers) because real-time notification systems are inherently asynchronous. You will need to choose a transport mechanism, so familiarity with WebSockets at a conceptual level is helpful but not required.

Step 1: Choose Your Transport Mechanism

Real-time delivery requires a persistent connection between the client and server, or a sufficiently fast polling mechanism that approximates real-time behaviour. The three main approaches are WebSockets, Server-Sent Events, and long polling. Each has trade-offs that matter in production.

WebSockets provide a full-duplex, persistent connection between client and server. Once the connection is established, either side can send messages at any time. This is the standard choice for real-time notifications because it has the lowest latency (messages arrive within milliseconds), supports bidirectional communication, and is efficient — a single connection handles all notifications rather than making repeated HTTP requests.

The complexity with WebSockets is infrastructure. Your web server must support persistent connections, which means either a dedicated WebSocket server or a reverse proxy configured to upgrade connections. Scaling WebSockets across multiple servers requires a pub/sub backend (Redis is typical) so that a notification generated on one server reaches users connected to a different server. Connection management — handling disconnects, reconnects, and authentication on reconnect — adds client-side complexity.

Server-Sent Events (SSE) provide a simpler, one-directional alternative. The server pushes messages to the client over a standard HTTP connection. SSE is easier to implement than WebSockets because it uses regular HTTP (no special server configuration), automatically handles reconnection, and works through most proxies and load balancers without configuration changes. The limitation is that it is one-directional: the server pushes to the client, but the client cannot send messages back over the same connection. For notifications, this is fine — notifications flow from server to client.

Long polling is the fallback for environments where persistent connections are impractical. The client makes an HTTP request, and the server holds the request open until there is a notification to deliver (or a timeout expires). The client immediately makes another request after receiving a response. This simulates real-time delivery with standard HTTP and works everywhere, but each notification requires a full HTTP round trip, and holding connections open on the server consumes resources.

Managed services abstract the transport layer entirely. Pusher, Ably, and similar services handle WebSocket infrastructure, scaling, connection management, and client libraries for multiple platforms. You publish a message to the service’s API, and they deliver it to connected clients. For most applications, a managed service is the right choice — the cost is modest and the operational burden of running WebSocket infrastructure is eliminated. The trade-off is vendor dependency and the latency of an additional network hop.

For most web applications, use a managed WebSocket service (Pusher or Ably) and move to self-hosted WebSockets only if cost or latency requirements demand it. For applications that only need server-to-client notifications and cannot use a managed service, SSE is simpler and more reliable than self-hosted WebSockets.

Step 2: Design Your Notification Channels

A notification channel is the medium through which a notification reaches the user. Most applications need multiple channels because users have different contexts and urgency levels.

In-app notifications appear within the application’s UI — a notification bell, a toast message, a badge count. These are the primary channel for updates that matter while the user is actively using the application. In-app notifications should be stored in a notifications table so users can see a history of recent notifications, mark them as read, and catch up after being away. The real-time transport delivers the notification for immediate display; the database provides persistence.

Push notifications reach users on their mobile devices or desktop browsers when they are not actively using the application. Push is the appropriate channel for time-sensitive updates that require attention: a payment received, a task assigned, a system alert. Push notifications require platform-specific setup (Apple Push Notification Service for iOS, Firebase Cloud Messaging for Android and web). The critical constraint with push is restraint — users who receive too many push notifications disable them entirely, which means your most important notifications lose their delivery channel.

Email notifications cover events where the user may not see the notification for hours or days, or where a written record is valuable. Email is appropriate for daily summaries, weekly reports, invoices, and confirmations. It is not appropriate for real-time alerts (by the time the email arrives and is read, the moment has passed) or high-frequency updates (a notification for every message in a chat would flood the inbox).

SMS notifications are reserved for critical alerts that demand immediate attention regardless of whether the user is online: security alerts (login from a new device), system outages affecting their business, and two-factor authentication codes. SMS has a direct cost per message and a low tolerance from users for non-critical content.

Design your channels around urgency and context. A new comment on a project might warrant an in-app notification and an email if the user has been away for more than an hour. A failed payment might warrant in-app, push, and email. A marketing update should not use push or SMS at all.

Step 3: Build the Notification Pipeline

The notification pipeline is the server-side system that decides what to send, where to send it, and how to send it. Separating these concerns keeps the system maintainable as the number of notification types and channels grows.

Events trigger notifications. When something happens in your application (a payment is received, a comment is posted, a task is assigned), it fires a domain event. The notification system listens for these events and determines which notifications to generate. The event itself should not know about notifications — it describes what happened, not how to communicate it.

The notification class defines what the notification contains and which channels it should be delivered through. Each notification type specifies its content (title, body, action URL, metadata), its target channels (in-app, push, email), and any channel-specific formatting. A payment notification might have a short body for push (“Payment of one hundred and fifty pounds received”) and a longer body for email (with invoice details, payment method, and a link to the receipt).

Queue every delivery. Notification delivery should be asynchronous. Generating a notification in the same request that triggers the event adds latency to the user action and creates a failure coupling — if the push notification service is slow, the user’s request is slow. Dispatch notification jobs to a queue and let workers handle delivery independently. This also provides natural retry handling: if an email fails to send, the queue worker retries it without affecting the triggering request.

Broadcast for real-time delivery. For in-app notifications, broadcast the notification to the user’s channel using your WebSocket transport. The client listens on a private channel (scoped to the authenticated user) and updates the UI when a notification arrives. The broadcast payload should include enough information to render the notification without a follow-up API call — the notification type, title, body, and action URL at minimum.

Batch where appropriate. Not every event needs an immediate notification. If a user receives ten comments in five minutes, sending ten separate push notifications is hostile. Implement batching logic that groups related notifications within a time window and delivers a single summary (“You have ten new comments on your project”) instead of individual notifications. The batching window depends on the notification type — chat messages might batch within thirty seconds, while daily activity summaries batch over twenty-four hours.

Step 4: Implement User Preferences

Notification preferences are not a nice-to-have feature — they are the mechanism that prevents users from disabling notifications entirely. A user who cannot control their notification channels will mute or unsubscribe from everything, which is worse for engagement than letting them fine-tune their preferences.

Preference granularity determines how much control users have. At minimum, allow users to enable or disable each channel (in-app, push, email) per notification type. Some notification types should not be disableable (security alerts, for example), but most should respect user preferences. A settings page with a grid of notification types and channels, where each cell is a toggle, is the standard pattern.

Default preferences should be conservative. Enable in-app for everything, push for time-sensitive events only, and email for infrequent summaries. Users who want more notifications can opt in. Users who receive too many notifications from the start opt out of everything and do not come back to adjust individual settings.

Quiet hours allow users to suppress non-critical notifications during specified time periods. This is particularly important for push and SMS channels. A task assignment at 2am does not need to wake someone up — it can wait until morning. Implement quiet hours as a time window during which non-critical push and SMS notifications are held and delivered when the window ends.

Check preferences before sending. The notification pipeline should check user preferences before dispatching to any channel. This check belongs in the notification system, not in the individual channel handlers. Centralising the preference check ensures consistency and makes it easy to audit which notifications a user will receive.

Respect platform conventions. iOS and Android have their own notification permission systems. When a user denies push notification permission at the OS level, your application should detect this and update its UI accordingly (do not show a push notification toggle for a user who has denied permission at the system level). Provide instructions for re-enabling permissions when users change their mind.

Step 5: Handle Edge Cases and Scale

Production notification systems encounter edge cases that test environments never produce. Handling these gracefully is what separates a notification system that users trust from one they disable.

Duplicate prevention. The same event can trigger multiple times due to retries, race conditions, or bugs. Your notification pipeline must be idempotent — processing the same event twice should not produce duplicate notifications. Use the event identifier to detect and skip duplicates before generating notifications.

Delivery failure handling. Push notification services return errors for invalid tokens (the user uninstalled the app), expired tokens, and service outages. Handle each case: remove invalid tokens from your database so you stop sending to them, refresh expired tokens where possible, and retry on transient failures with exponential backoff. Failed email delivery should trigger a retry (transient errors) or remove the address (permanent bounces).

Connection management for real-time. Users open multiple tabs, switch between devices, and have intermittent connectivity. Your WebSocket implementation must handle: multiple connections per user (broadcast to all of them, not just the first), reconnection after network interruption (the client should automatically reconnect and receive missed notifications), and authentication on reconnect (a WebSocket connection opened before a session expires should re-authenticate on reconnect).

Notification storage and cleanup. In-app notifications accumulate in your database. Implement a retention policy: keep notifications for ninety days, or keep the most recent one hundred per user, and delete older entries. Without cleanup, the notifications table grows indefinitely. Index the table on user ID and created timestamp for efficient querying and deletion.

Rate limiting outbound notifications. A bug that triggers thousands of notifications will exhaust your email quota, exceed your push notification budget, and destroy user trust in a single incident. Implement a rate limit on outbound notifications per user per channel. If a user would receive more than a threshold number of notifications within a time window, batch the excess into a summary and alert your engineering team about the anomaly.

Common Mistakes

  • Sending push notifications for everything. Push is a high-trust channel. Overuse leads to users disabling notifications entirely, which means your critical alerts lose their delivery mechanism. Reserve push for events that genuinely need the user’s immediate attention.
  • No preference system. Users have different thresholds for notification volume. Without preferences, you either send too many (and users mute everything) or too few (and users miss important updates). Let users control their channels.
  • Synchronous notification delivery. Sending notifications in the same request that triggers the event adds latency and creates failure coupling. Queue all notification delivery.
  • No duplicate prevention. Events can fire multiple times. Without idempotency in your notification pipeline, users receive the same notification repeatedly, which erodes trust.
  • Ignoring invalid push tokens. Sending to invalid tokens wastes API calls and can affect your delivery reputation with push notification services. Clean up invalid tokens promptly.

What Good Looks Like

A well-implemented notification system has: a managed WebSocket service for real-time in-app delivery, a queued pipeline that separates event detection from notification delivery, multiple channels (in-app, push, email) with content tailored to each, user preferences that control which channels receive which notification types, batching that prevents notification fatigue, duplicate prevention at the pipeline level, and monitoring that catches delivery failures and anomalous volumes. Users receive timely, relevant notifications through their preferred channels, and the system degrades gracefully when any single channel experiences issues.

Next Steps

For the API design that notification endpoints should follow, How to Structure a REST API covers the patterns for notification retrieval and preference management. For the broader system architecture that notifications plug into, How to Configure Server Monitoring covers monitoring the infrastructure that your notification pipeline runs on.

Need Hands-On Help?

Our guides give you the thinking. If you want someone to do the building, we should talk.

Start a Project Browse Case Studies