You want a live Bitcoin price on your website. Not a blurry screenshot, not a widget that calls home to some third-party analytics every render. An actual ticker that updates in near real-time, handles reconnection, and doesn't break when the source API hiccups. This post walks through building exactly that in under 20 lines of JavaScript, plus the production details you need before shipping.

Everything here uses free public endpoints. No API key, no signup, no rate-limited free tier.

The Minimum Viable Ticker

Start with the HTML. One element to hold the price, one to show the timestamp of the last update.

<div class="btc-ticker"> <span class="btc-label">BTC/USDT</span> <span id="btc-price">...</span> <span id="btc-time" class="btc-time"></span> </div>

Now the script. We connect to Binance's public WebSocket trade stream, parse each incoming trade, and render the price.

const priceEl = document.getElementById('btc-price'); const timeEl = document.getElementById('btc-time'); const ws = new WebSocket('wss://stream.binance.com:9443/ws/btcusdt@trade'); ws.onmessage = (event) => { const trade = JSON.parse(event.data); const price = parseFloat(trade.p); if (!isFinite(price)) return; priceEl.textContent = '$' + price.toFixed(2); timeEl.textContent = new Date(trade.T).toLocaleTimeString(); };

That is a working Bitcoin ticker. Drop it on any page with a pulse and it will update every second or two during active trading. It does not handle reconnection, does not fall back to REST if the WebSocket fails, and does not pause when the tab is hidden. For a casual page, this might be enough. For anything you care about, keep reading.

Adding the REST Fallback

WebSocket won't open on every network. Corporate firewalls block the upgrade handshake. Some mobile networks throttle persistent connections. A robust ticker falls back to REST polling when the WebSocket can't establish.

let pollTimer = null; function pollREST() { fetch('https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT') .then(r => r.json()) .then(data => { const price = parseFloat(data?.price); if (!isFinite(price)) return; priceEl.textContent = '$' + price.toFixed(2); timeEl.textContent = new Date().toLocaleTimeString(); }) .catch(() => {}); } function startPolling() { if (pollTimer) return; pollREST(); pollTimer = setInterval(pollREST, 3000); } function stopPolling() { clearInterval(pollTimer); pollTimer = null; } ws.onopen = stopPolling; ws.onerror = startPolling; ws.onclose = startPolling;

Now if the WebSocket fails to open, we poll every 3 seconds. If it opens, we stop polling and rely on the stream. Simple and bulletproof.

Reconnection

WebSockets die. The TCP connection can be silently closed by a proxy, a mobile radio handoff, or the server itself. When the socket closes, we need to reopen it, but not instantly and not infinitely.

let reconnectDelay = 1000; let reconnectTimer = null; let socket = null; function connect() { socket = new WebSocket('wss://stream.binance.com:9443/ws/btcusdt@trade'); socket.onopen = () => { reconnectDelay = 1000; stopPolling(); }; socket.onmessage = (event) => { const trade = JSON.parse(event.data); const price = parseFloat(trade.p); if (!isFinite(price)) return; priceEl.textContent = '$' + price.toFixed(2); timeEl.textContent = new Date(trade.T).toLocaleTimeString(); }; socket.onclose = () => { startPolling(); reconnectTimer = setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 2, 30000); }; socket.onerror = () => { socket.close(); }; } connect();

The exponential backoff caps at 30 seconds so we do not hammer the server during an outage. REST polling keeps the UI fresh during the gap. When the WebSocket recovers, polling stops and we're back to push-based updates.

Tab Visibility and Battery

When the user switches tabs, the ticker is invisible. Keeping the WebSocket open and re-rendering every second is a waste of their battery and our bandwidth. Listen for the visibility event and pause when the page is hidden.

document.addEventListener('visibilitychange', () => { if (document.hidden) { stopPolling(); if (socket) socket.close(); clearTimeout(reconnectTimer); } else { connect(); pollREST(); // immediate refresh on return } });

This single listener saves hours of CPU and radio time on users who leave 40 tabs open.

Null-Safe Defaults

Never trust external API response shapes. Every field can be undefined, null, or a different type than documented. This caused a full site crash on TerminalFeed in April 2026 when a panel called .toFixed() on an undefined price field. The fix is cheap: null-safe defaults before any method call.

// unsafe, crashes if price is undefined priceEl.textContent = '$' + data.price.toFixed(2); // safe, renders 'N/A' if the field is missing const price = parseFloat(data?.price); priceEl.textContent = isFinite(price) ? '$' + price.toFixed(2) : 'N/A';

The rule: value ?? fallback BEFORE calling any method on it. Or use optional chaining and isFinite / type guards. Never assume the field exists.

Color-Coded Price Changes

A ticker that shows only the current price is half a ticker. The interesting information is whether the price went up or down since the last tick. Track the previous price and flash the element green or red on change.

let lastPrice = null; function render(price) { priceEl.textContent = '$' + price.toFixed(2); if (lastPrice !== null) { priceEl.classList.remove('up', 'down'); if (price > lastPrice) priceEl.classList.add('up'); else if (price < lastPrice) priceEl.classList.add('down'); } lastPrice = price; }

Paired with CSS for .up { color: #4ADE80 } and .down { color: #F87171 }, you now have a ticker that visually signals direction.

Throttling Re-Renders

During volatile minutes, Binance can push 20+ trade events per second. Re-rendering the DOM 20 times per second is wasteful and causes layout thrash. Throttle paints to once per second using a simple flag.

let pendingPrice = null; let renderPending = false; function queuePrice(price) { pendingPrice = price; if (renderPending) return; renderPending = true; setTimeout(() => { if (pendingPrice !== null) render(pendingPrice); renderPending = false; }, 1000); }

One paint per second is the sweet spot: fast enough to feel live, slow enough to stay smooth.

Putting It All Together

A production-quality Bitcoin ticker is roughly 60 lines of JavaScript with the reconnect, fallback, visibility, null-safety, color coding, and throttle logic. It costs you nothing to run because Binance's public endpoints are free and have no meaningful rate limits for client-side use.

If you want to see the full architecture in context, TerminalFeed's BTC hero uses exactly these patterns: Binance WebSocket primary, CoinCap REST fallback, 1-second paint throttle on desktop and 3-second on mobile, visibility-based pausing, null-safe defaults everywhere. We explain the WebSocket vs polling decision in more depth in Why We Built a WebSocket Bitcoin Ticker Instead of Polling, and the mobile-specific gotchas in Why Your Mobile Bitcoin Ticker Lies.

Using the TerminalFeed API Instead

If you would rather not maintain your own WebSocket client, TerminalFeed exposes a free REST endpoint at https://terminalfeed.io/api/btc-price that already does the WebSocket plumbing, caching, and fallback on our side. It returns a simple JSON object with the latest price and a source field.

async function refresh() { const res = await fetch('https://terminalfeed.io/api/btc-price'); const data = await res.json(); priceEl.textContent = '$' + (data?.price ?? 0).toFixed(2); } setInterval(refresh, 3000); refresh();

CORS is enabled on all our /api/* routes so you can call it directly from the browser. Free, no key, no rate limit beyond common sense.

What Not to Do

Use the TerminalFeed API

Free Bitcoin price, crypto movers, stocks, forex, prediction markets, and 20+ more endpoints. CORS-enabled, no API key required.

API Docs Live Dashboard