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.

- 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.
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 stream | REST polling | |
|---|---|---|
| Best for | Order books, ticks many times per second | Data refreshed on a fixed interval |
| Infrastructure | Persistent connections, reconnect logic, heartbeats | Plain HTTP, caches, CDNs |
| Debugging | Harder | curl and logs |
| Cost per user | One connection per client to the provider (or a relay) | Zero if you cache and fan out |
| Fit for a 60 s aggregate | Overkill | Ideal |
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

- One poller requests
/v1/rateseveryfreshness_sec. - It keeps the last snapshot in memory (or Redis for several instances).
- It computes which symbols changed.
- It pushes changes to connected clients; new clients receive the full snapshot.
Node.js poller with Server-Sent Events
// 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
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.
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.

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


