You open your crypto app on your phone. A Bitcoin ticker loads. You glance at the number, put the phone down, pick it up a minute later, and the number is exactly the same. Either BTC hasn't moved a cent in 60 seconds, which is extremely unlikely, or your mobile ticker is lying.

Mobile browsers and native apps behave very differently from a desktop tab. Every ticker that feels fast on a laptop has subtle failure modes on a phone. This post walks through the three big ones, and the defensive patterns that fix them.

Failure 1: Tab Suspension

When you switch apps or lock your phone, both iOS Safari and Chrome on Android aggressively suspend background tabs. Timers stop firing. WebSocket connections are kept open at the TCP level in some cases, but message handlers don't run. From the JavaScript's point of view, time stops.

When you refocus the tab, the ticker's last rendered price is still whatever it was when you switched away. The element shows that number confidently. There is no loading state, no stale indicator, nothing to tell you the feed is behind. The browser resumes JS execution and the ticker starts updating again within a second, but for one long second, you're looking at a lie.

The fix is a visibilitychange listener that treats regained visibility as a trigger for a fresh fetch.

document.addEventListener('visibilitychange', () => { if (!document.hidden) { // immediate REST refresh so user sees fresh price fetchBtcPrice().then(renderPrice); // then resume WebSocket reconnectSocket(); } else { // close socket and stop timers to save battery pauseUpdates(); } });

This pattern is non-negotiable on mobile. Without it, every ticker on every phone lies for a beat after refocus. With it, the user sees a fresh price almost instantly when they come back.

Failure 2: Radio Handoff

Your phone's network changes constantly. WiFi to cell, cell to WiFi, one tower to another, 5G to 4G when you step into an elevator. Each handoff breaks open TCP connections. The WebSocket was alive on WiFi; the handoff happens; the new connection is on LTE; the old socket is dead.

The browser doesn't always notice quickly. From the app's perspective, the WebSocket looks open but no messages arrive. Ten seconds, twenty seconds pass with no updates. The ticker freezes.

The fix is a heartbeat. Expect at least one message within N seconds. If the deadline passes with no activity, assume the connection is dead, close it, and reconnect.

let lastMessageAt = Date.now(); let heartbeatTimer = null; socket.onmessage = (event) => { lastMessageAt = Date.now(); handleMessage(event); }; heartbeatTimer = setInterval(() => { const gap = Date.now() - lastMessageAt; if (gap > 15000) { // assume dead, force reconnect socket.close(); } }, 5000);

15 seconds is a conservative threshold for a BTC stream during active hours. During low-activity periods (weekend pre-dawn hours), Binance may genuinely send no trades for 10+ seconds. Tune based on your feed.

Failure 3: Battery Throttling

iOS and Android actively throttle JavaScript execution on backgrounded or low-power tabs. A setInterval(fn, 1000) on the foreground tab fires every second. The same interval on a phone in low-power mode might fire every 5 seconds or pause entirely.

The OS does this to save battery. From a ticker's perspective, it means even if the tab is technically "active," your polling cadence is a suggestion, not a guarantee. And if you fight it, you lose: running a high-frequency timer is exactly the pattern battery-savers are designed to kill.

The fix is not to fight it. Throttle more aggressively on mobile. Accept slower updates. Pair that with a clear "last updated" timestamp so the user can see the actual cadence.

TerminalFeed's approach: desktop paints at 1 second, mobile at 3 seconds. WebSocket throttled at the render layer, not the socket layer. The socket still receives every trade event, the renderer just batches.

Failure 4: Silent Stream Failures

Mobile networks drop packets quietly. A WebSocket connection may silently lose a message without any retransmission at the application layer. Your client sees a gap in the price stream but has no way to know it's a gap.

Mitigation: always display a "last updated" timestamp prominently. If the user sees "2 minutes ago" next to the price, they know the number is stale before they act on it. If the UI just shows the price with no time context, they have no defense.

function renderPrice(price, ts) { priceEl.textContent = '$' + price.toFixed(2); const age = Math.floor((Date.now() - ts) / 1000); timeEl.textContent = age < 5 ? 'just now' : age < 60 ? age + 's ago' : Math.floor(age / 60) + 'm ago'; // visual staleness indicator priceEl.classList.toggle('stale', age > 30); }

Combined with a CSS rule like .stale { color: #8A8880 }, the UI visually dims when the feed falls behind. The user sees the problem without needing to read the timestamp.

Failure 5: Animation and Layout Thrash

Every pixel you paint on mobile costs battery and frame budget. A ticker that pulses, animates, or re-renders adjacent elements on every tick turns into a performance problem when layered into a dashboard with 30 panels.

Rules we enforce on mobile:

Failure 6: The Zombie Connection

When the user navigates away from the ticker page to another page inside your SPA, the WebSocket should close. If it doesn't, it keeps running in the background, using battery and bandwidth, never surfacing data anywhere. Users open five tabs, each spawns a BTC WebSocket, none close, the phone gets hot.

Always close sockets in cleanup:

useEffect(() => { const socket = new WebSocket(BTC_STREAM); socket.onmessage = handleMessage; return () => { socket.close(); // also clear any timers }; }, []);

For non-React apps, attach a beforeunload or pagehide handler that closes the socket. It's one of the easiest resource leaks to miss and one of the most frequent.

What TerminalFeed Does on Mobile

Our mobile BTC hero follows the same WebSocket-primary, REST-fallback pattern as desktop, with these adjustments:

The Pattern, Condensed

Mobile tickers are a trust problem. The user cannot tell when a price is stale. They will make decisions based on what they see. Build accordingly:

  1. Always show a "last updated" timestamp or age indicator.
  2. Refresh immediately on tab refocus.
  3. Heartbeat-driven reconnect on dead sockets.
  4. Throttle paints to conserve battery.
  5. Visually dim stale values.
  6. Clean up on unmount, always.

A ticker that follows all six rules tells the truth. A ticker that skips any of them lies sometimes, and you will not know when.

For the desktop architecture, see Why We Built a WebSocket Bitcoin Ticker Instead of Polling. For the full code walkthrough, How to Add a Free Bitcoin Ticker to Your Website.

Test the Ticker on Your Phone

Open TerminalFeed on mobile, background the tab, come back. The ticker refreshes immediately and tells you how old the last update is.

Open on Mobile API Docs