EZRate
Sign in

Currency Exchange WebSocket API or REST Polling? A Node.js Guide

Do you need a currency exchange WebSocket API? Compare streaming and REST polling for FX data updated every minute and build a Node.js poller with SSE fan-out.

Arian Asadi
· Writer and Market Analyst
Published 2 November 2026, 20:00 · 3 min read
Currency Exchange WebSocket API or REST Polling?
Key takeaways
  • WebSockets shine when data changes many times per second; a feed that aggregates every 60 seconds gains almost nothing from streaming.
  • The efficient pattern: one server polls the REST API once per freshness window and pushes changes to clients over Server-Sent Events or WebSocket.
  • This keeps the API key on the server, uses one request per minute regardless of visitor count and gives users instant updates.

Developers often ask for a currency exchange WebSocket API because “real time” sounds like streaming. Whether you need one depends on how often the underlying data actually changes. This article compares the two approaches for Iranian market rates and builds a Node.js service that gives your users push updates while making just one REST call per minute.

i
EZRate API
This guide uses the EZRate currency and gold price API: full documentation, code samples and a free key are on the API page.
★
Short answer
EZRate rates are aggregated every 60 seconds, so the API is REST-only and there is no WebSocket stream. Poll GET /v1/rates from one server process every meta.freshness_sec seconds, compare with the previous snapshot and push only changes to browsers using Server-Sent Events (or your own WebSocket server). Clients get instant updates; your quota use stays constant.

Part of the Iranian Rial exchange rate API guide.

Streaming vs polling

WebSocket streamREST polling
Best forOrder books, ticks many times per secondData refreshed on a fixed interval
InfrastructurePersistent connections, reconnect logic, heartbeatsPlain HTTP, caches, CDNs
DebuggingHardercurl and logs
Cost per userOne connection per client to the provider (or a relay)Zero if you cache and fan out
Fit for a 60 s aggregateOverkillIdeal

A WebSocket from the provider cannot deliver a new aggregated rate faster than the aggregation itself. What users really want is not to refresh the page — and you can give them that with a relay.

Architecture

Diagram: Node.js poller calls EZRate REST every minute, stores the snapshot and pushes diffs to browsers over SSE
One poller, many live clients
  1. One poller requests /v1/rates every freshness_sec.
  2. It keeps the last snapshot in memory (or Redis for several instances).
  3. It computes which symbols changed.
  4. It pushes changes to connected clients; new clients receive the full snapshot.

Node.js poller with Server-Sent Events

javascript
// server.mjs — Node 20+
import http from "node:http";

const KEY = process.env.EZRATE_KEY;
const URL = "https://api.ezrate.me/v1/rates?symbols=usd,eur,aed,try,g18,emami,usdt";
let snapshot = {};           // symbol -> rate
let interval = 60_000;
const clients = new Set();

async function poll() {
  try {
    const r = await fetch(URL, { headers: { "X-API-Key": KEY }, signal: AbortSignal.timeout(10_000) });
    if (r.status === 429) {
      interval = Number(r.headers.get("retry-after") ?? 60) * 1000;
      return;
    }
    const { data, meta } = await r.json();
    interval = Math.max(30, meta.freshness_sec) * 1000;
    const changed = data.filter(x => snapshot[x.symbol]?.sell !== x.sell || snapshot[x.symbol]?.buy !== x.buy);
    for (const x of data) snapshot[x.symbol] = x;
    if (changed.length) broadcast({ type: "update", rates: changed, as_of: meta.as_of });
  } catch (e) {
    console.error("poll failed", e.message);   // keep serving the last snapshot
  } finally {
    setTimeout(poll, interval);
  }
}

function broadcast(msg) {
  const line = `data: ${JSON.stringify(msg)}\n\n`;
  for (const res of clients) res.write(line);
}

http.createServer((req, res) => {
  if (req.url !== "/stream") return res.writeHead(404).end();
  res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
  res.write(`data: ${JSON.stringify({ type: "snapshot", rates: Object.values(snapshot) })}\n\n`);
  clients.add(res);
  const ping = setInterval(() => res.write(": ping\n\n"), 25_000);
  req.on("close", () => { clearInterval(ping); clients.delete(res); });
}).listen(8080);

poll();

Browser client

javascript
const es = new EventSource("/stream");
es.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  for (const r of msg.rates) {
    const el = document.querySelector(`[data-sym="${r.symbol}"]`);
    if (!el) continue;
    el.textContent = new Intl.NumberFormat("en-US").format(r.sell);
    el.classList.toggle("up", r.change_pct > 0);
    el.classList.toggle("down", r.change_pct < 0);
  }
};

EventSource reconnects automatically. If you already run a WebSocket server (Socket.IO, ws), call io.emit instead of broadcast — the upstream side stays the same.

Scaling to several instances

  • Run the poller once (a separate worker or a leader lock in Redis), not in every web process.
  • Publish updates on Redis pub/sub; each web instance relays to its own clients.
  • Put the snapshot in Redis so new instances start warm. See the caching patterns in Laravel exchange rate API caching.

Failure handling

If polls fail, clients keep the last values; send a stale message after a few missed cycles so the UI can show a warning. Respect Retry-After on 429. Details in financial API failover.

US Dollar271,740Toman−0.02%

When streaming would make sense

A WebSocket feed is the right tool when the data changes many times per second and every tick matters: an exchange order book, a matching engine, a live auction. Free-market rates in Iran are a different kind of data: the reliable signal is an aggregate built every minute from many quotes. Streaming each raw quote would show users noise, not information. If you run your own exchange alongside the aggregated rate, stream your order book from your own infrastructure and use the REST aggregate as a reference.

Operational tips

  • Behind Nginx, disable buffering for the SSE route (proxy_buffering off;) or events will arrive in bursts.
  • Send a comment ping every 20–30 seconds to keep idle connections alive through proxies.
  • Cap the number of connections per IP to protect the relay.
  • Include the snapshot time in each message so clients can detect staleness.

Related live rates: US dollar rate, 18K gold price, Emami coin price.

Does EZRate offer a WebSocket API?
No. Rates are aggregated every 60 seconds, so the API is REST. Use a server-side poller and push updates to your clients.
How often should I poll?
Once per meta.freshness_sec, from a single server process.
SSE or WebSocket for my clients?
SSE is simpler for one-way price updates and reconnects automatically; WebSocket is fine if you already use it.
Will polling exhaust my quota?
Not with one poller: it uses about one request per minute regardless of how many users are connected.
Arian Asadi
I studied economics and developed an interest in financial markets, exchange rates, and economic developments during my university years. Throughout my professional career, I have focused particularly on foreign exchange markets, gold, international money transfers, and the impact of economic developments on prices. In the articles I write, I strive to present information about exchange rates and financial markets in a clear and accessible way, based on verifiable data and market trends. My goal is to help readers gain a clearer understanding of market conditions before making decisions about buying, selling, or transferring currencies.
All posts by author →

Comments (0)

Comments are shown after review. Sign-in is needed to send.

Related

API & Developers
Financial API Failover Strategy: Timeouts, 429s, Staleness and Fallbacks
API & Developers
Live Exchange Rates in Google Sheets and Excel via API (No Add-ons)
API & Developers
Historical USD to IRR Data: Free-Market Candles via API for Analysts