Polling, realtime and performance

How the SDKs fetch notifications, how quickly new ones appear, and what that costs in API requests.

The SDKs always read the feed from one endpoint. By default they fetch it when they start, when you call refresh() and when the user comes back to your app. To keep an open screen up to date, turn on one or both of:

  • Realtime (Growth plan and up): a WebSocket tells the SDK when the user's feed changed, and it fetches the feed again. New notifications appear within seconds.
  • Polling: the SDK fetches the feed on a timer. An unchanged feed comes back as an empty 304 Not Modified, but every poll counts as an API request.

Polling pauses while the realtime connection is open, so asking for both makes polling the fallback. Nothing polls while your app is hidden, and the realtime connection closes a minute after it was hidden.

const client = createNotificationClient({ publishableKey, user, token });
const stopRealtime = client.realtime();
const stopPolling = client.poll(60_000);
// React and React Native, anywhere under <NotificationProvider>
function LiveNotifications() {
  useRealtime();
  usePolling(60_000);
  return null;
}

How quickly do notifications appear?

AudienceRealtimePolling
userssecondsthe poll interval, up to 5 minutes after a quiet spell
all, tags, segmentup to 30 seconds (refetches are spread out)the same, plus up to 30 seconds

Without either, new notifications appear on the next fetch: when the user returns to the tab or app, reloads it, or your code calls refresh().

  • The poll interval is what you pass to poll() / usePolling(), in milliseconds. The minimum is 10 seconds; shorter values are raised to 10 seconds. While nothing changes, the interval grows up to 5 minutes (see idle backoff).
  • Broadcasts (all, tags, segment) are cached per project for up to 30 seconds, so with polling they can take up to 30 seconds longer to appear than a users notification. Realtime signals skip this cache.
  • When a user returns to the tab or app (focus, visibility, back online), the SDK refetches right away if its last fetch is at least 10 seconds old.
  • Scheduled notifications appear from their publishAt on; the cache doesn't delay them.

Realtime

client.realtime() (or useRealtime()) opens a WebSocket to /v1/client/realtime. It carries no notifications, only a signal that the feed changed; the SDK then fetches the feed through the usual endpoint. state.realtime is off, connecting or open.

The server signals a user when:

  • a notification that reaches them is sent, edited, canceled or deleted, from the API or the dashboard (scheduled notifications when they publish);
  • they read, archive or click something on any of their devices;
  • the project user changes through the API or dashboard (tags, attributes, or it is deleted);
  • a segment is edited or deleted (everyone connected).

Broadcast audiences are matched when each feed loads, so a broadcast signals every connected user of the project, including users it doesn't reach, and each of them refetches. Those refetches are spread over a window that grows with the number of connections, up to 30 seconds.

  • Visibility. One connection per browser, held by the leader tab. It opens while any tab is visible and closes a minute after the last one was hidden. React Native connects only while the app is in the foreground. Coming back while the connection stayed open needs no refetch.
  • Reconnects. After a disconnect the SDK fetches the feed once (nothing is replayed) and reconnects with backoff, polling meanwhile if you asked for it. It reconnects with a fresh token before the current one expires, and pings every minute to notice dead connections.
  • Refused connections. On plans without realtime, or from an origin the project doesn't allow, the SDK stops trying and sets error (code forbidden) while the feed keeps working. Without an active subscription it behaves as for 402 payment_required. Over the monthly request limit it waits 10 minutes before trying again.

Requests and efficiency

Request usage

Every feed fetch counts towards your plan's monthly API requests, 304 Not Modified replies included. What adds up:

  • Polling: one request per poll while your app is visible. With poll(60_000) that is up to 60 an hour, dropping to 12 an hour after about 20 quiet minutes (idle backoff).
  • Realtime: one request when the connection opens, plus one feed refetch per (re)connect and per signal; pings and signals themselves are free. With a token function, the SDK reconnects with a fresh token shortly before the current one expires (about once an hour with the Node SDK's default), and it reconnects when your app becomes visible after more than a minute in the background.
  • Broadcasts and segment edits make every connected user refetch, not only the users they reach.
  • Coming back to the tab or app refetches if the last fetch is at least 10 seconds old.

CORS preflights and rejected requests don't count.

Cached CORS preflights

The credentials travel in headers (X-Publishable-Key and Authorization), never in the URL, so they don't end up in logs or browser history. Those headers make browsers send an OPTIONS preflight before a cross-origin request, but the API lets browsers cache the answer per URL for up to 24 hours (Access-Control-Max-Age: 86400), and browsers cap it further (Chrome at 2 hours). A polled feed URL therefore needs a preflight at most every couple of hours, not on every poll. Preflights don't count towards your request limit.

ETag revalidation

Feed responses carry a weak ETag and Cache-Control: private, no-cache. The browser's own HTTP cache stores the last response and revalidates it with If-None-Match; when nothing changed the server answers 304 with no body, and fetch transparently hands the cached body back to the SDK. An unchanged feed therefore costs your users almost no bandwidth, though it still counts as a request. Responses vary on Authorization, so two users of one browser never share a cached feed.

Visibility-aware polling

  • The JavaScript and React SDKs poll only while the page is visible; React Native polls only while the app is in the foreground (AppState).
  • Polling pauses while offline and resumes as soon as the connection is back.
  • Idle backoff: after 3 unchanged responses in a row the interval doubles, up to 5 minutes. Any change or interaction (an action, a filter change, refocusing) resets it.
  • Error backoff: exponential with jitter, capped at 5 minutes, honouring Retry-After. Without polling, only a failed first load is retried.

One poller per browser

Tabs of the same user elect a leader tab with the Web Locks API. Only the leader polls and holds the realtime connection; it shares each result with the other tabs over BroadcastChannel, and actions in one tab show up in all of them. The leader keeps going while any tab is visible. When it closes, another tab takes over. Ten open tabs cost the same as one.

Cached feeds

The SDKs keep the last feed page, the project config and the last synced profile in localStorage (or AsyncStorage on React Native), so a page reload shows notifications instantly and revalidates in the background. The profile is only sent again when it changed.

Tips

  • Keep users who sit on the same screen up to date with realtime. It is faster than polling and usually cheaper: a request per change instead of one every interval.
  • Poll only as a fallback (Starter plan, or while realtime reconnects). 60 seconds is a good start; halving the interval doubles the request volume of users who keep your app open.
  • Render the bell once per page and share the client (the React and React Native providers do this for you).
  • For digest emails or server-rendered inboxes, use GET /v1/project-users/{userId}/feed with your secret key. It supports If-None-Match too, and server API calls don't count towards your request limit.