---
title: "Live FX Rates API: Polling vs WebSocket, and How to Poll Efficiently"
description: "What live means in a live FX rates API, polling REST vs WebSocket streaming, when each is needed, and Node code that polls once and fans out via SSE."
canonical: https://allratestoday.com/blog/live-fx-rates-api-polling-vs-websocket/
source: AllRatesToday (https://allratestoday.com/) — Markdown mirror of the page above; index of all pages at https://allratestoday.com/llms.txt
generated: 2026-11-11T15:57:43.445Z
---

A **live FX rates API** sounds like one thing, but "live" covers everything from a tick-by-tick trading stream to a rate that changes once a minute. Which one you need depends on what your app does with the number. This tutorial explains the difference, when polling a REST endpoint is the right design, and how to do it with one API call per minute no matter how many users you have.

To be clear up front: AllRatesToday is REST only. There is no WebSocket and no push feed. Each rate is a mid-market snapshot refreshed every 60 seconds. The code below shows how to turn that into a live feed for your own clients.

## What "live" means for FX rates

There are three common meanings:

| Kind | Update interval | Contains | Delivery |
| --- | --- | --- | --- |
| Tick stream | Every price change, many per second for major pairs | Bid and ask | WebSocket or HTTP stream from a broker or market data vendor |
| Snapshot | Every minute (or hourly, daily) | One mid-market rate per pair | REST |
| Fixing | Once a day or month | One official rate | REST or a file download |

A tick stream is what a trading screen needs. A snapshot is what a shop, a billing system or a dashboard needs. Nobody re-prices a basket of shoes eight times a second.

## Live FX rates API: polling REST vs WebSocket

**Polling** means your code calls a REST endpoint on a timer. It is stateless, works through every proxy and firewall, is easy to cache, and fails in a simple way: if one call fails, you keep the last value and try again next minute.

**WebSocket or streaming** means the server holds a connection open and pushes each change. It has lower latency and no wasted calls. It also needs reconnect logic, heartbeat handling, back-pressure, and a plan for what happens when the stream silently stops.

Use this rule:

- **Trading, order entry, tick charts:** streaming from a broker or market data vendor. You need bid and ask, and latency matters.
- **Pricing, checkout, invoices, dashboards, alerts:** polling a snapshot API. One-minute freshness is more than enough, and the rate is the same for every user.

If your source updates every 60 seconds, a stream would only push one message a minute anyway. Polling every 60 seconds gets you the same data with less machinery.

## How to poll efficiently

Most of the cost of polling comes from doing it in the wrong place. Three rules:

1. **Poll from the server, once.** Not from each browser. A thousand open tabs should still mean one upstream call a minute.
2. **Ask for every currency in one call.** `target=EUR,GBP,JPY,CAD,AUD` returns one array and costs one request.
3. **Fan out yourself.** Keep the latest snapshot in memory (or Redis, or KV) and push it to clients over your own Server-Sent Events or WebSocket endpoint.

Do the arithmetic before you choose an interval. One call per minute is about 1,440 calls a day and about 43,200 in a 30-day month. On AllRatesToday that fits the Large plan (100,000 a month). Every 5 minutes is about 8,640 a month, which fits Medium (10,000). Every 15 minutes is about 2,880, which fits Small (5,000). The [caching guide](https://allratestoday.com/blog/how-to-cache-exchange-rates-avoid-rate-limits/) has more patterns.

## Node example: poll once, broadcast via SSE

This uses only Node 18+ built-ins: `fetch` and `http`. Save it as `server.mjs` and run `ALLRATES_API_KEY=YOUR_API_KEY node server.mjs`.

```
import http from "node:http";

const API_KEY = process.env.ALLRATES_API_KEY;
const BASE = "USD";
const TARGETS = ["EUR", "GBP", "JPY", "CAD", "AUD"];
const POLL_MS = 60_000;
const URL_RATES =
  `https://allratestoday.com/api/v1/rates?source=${BASE}&target=${TARGETS.join(",")}`;

let latest = null;            // last good snapshot
const clients = new Set();    // open SSE responses

function send(res, snapshot) {
  res.write(`event: rates\ndata: ${JSON.stringify(snapshot)}\n\n`);
}

async function poll() {
  try {
    const res = await fetch(URL_RATES, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    if (res.status === 429) {
      console.warn("Monthly limit reached, resets:", res.headers.get("X-Monthly-Resets"));
      return;                 // keep serving the last snapshot
    }
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const rows = await res.json();          // an array, one item per target
    const snapshot = {
      base: BASE,
      time: rows[0]?.time,
      fetchedAt: new Date().toISOString(),
      rates: Object.fromEntries(rows.map((r) => [r.target, r.rate])),
    };

    const changed =
      !latest || JSON.stringify(latest.rates) !== JSON.stringify(snapshot.rates);
    latest = snapshot;
    if (changed) for (const c of clients) send(c, snapshot);
  } catch (err) {
    console.error("Poll failed, keeping last snapshot:", err.message);
  }
}

const server = http.createServer((req, res) => {
  if (req.url === "/rates/stream") {
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    });
    res.write("retry: 5000\n\n");           // browser reconnect delay
    if (latest) send(res, latest);          // new clients get data at once
    clients.add(res);
    req.on("close", () => clients.delete(res));
    return;
  }
  if (req.url === "/rates") {               // plain JSON for non-SSE clients
    res.writeHead(latest ? 200 : 503, { "Content-Type": "application/json" });
    res.end(JSON.stringify(latest ?? { error: "warming up" }));
    return;
  }
  res.writeHead(404).end();
});

// Comment line every 25 s so proxies do not close idle streams.
setInterval(() => { for (const c of clients) c.write(": ping\n\n"); }, 25_000);

await poll();
setInterval(poll, POLL_MS);
server.listen(3000, () => console.log("http://localhost:3000/rates/stream"));
```

What this does:

- **One upstream call per interval**, whatever the number of connected clients.
- **Reads the array correctly.** `/api/v1/rates` always returns an array, so the code maps over `rows` rather than reading `res.rate`.
- **Keeps the last good snapshot** on errors and on 429, so clients never see an empty state after the first success.
- **Broadcasts only on change**, so idle periods cost nothing on the client side.
- **Carries the rate time.** `time` is when the rate refers to; `fetchedAt` is when you got it. Show or log both.

## The browser side

`EventSource` is built into every modern browser and reconnects on its own:

```
<span id="eur">…</span>
<script>
  const es = new EventSource("/rates/stream");
  es.addEventListener("rates", (e) => {
    const snap = JSON.parse(e.data);
    document.getElementById("eur").textContent =
      `1 USD = ${snap.rates.EUR.toFixed(4)} EUR (as of ${new Date(snap.time).toLocaleTimeString()})`;
  });
</script>
```

The API key stays on your server. The browser only talks to your endpoint.

## SSE or your own WebSocket?

SSE is one-way, server to client, over plain HTTP. That is exactly the shape of a rate feed. Use a WebSocket only if clients also need to send messages, for example to subscribe to a different set of currencies. In that case, keep the same design: one poller, one shared snapshot, and filter per subscription when you send.

If you run several server instances, put the snapshot in a shared store and let only one instance poll. A simple lock key with a 60-second expiry in Redis works, or a scheduled job that writes to the store while web instances only read.

## Serverless: a scheduled job instead of a timer

On serverless platforms there is no long-running process to hold a `setInterval`. Split the work in two:

- **A scheduled job** (a cron trigger, a scheduled function, or a cron entry on a small VM) calls `/api/v1/rates` once per interval and writes the snapshot to a shared store such as Redis, a key-value store or a database row.
- **Your request handlers** only read that stored snapshot. They never call the rates API.

Clients can then poll your own endpoint as often as they like, with a short cache header such as `Cache-Control: max-age=30`, or you can keep the SSE endpoint on a small always-on service. Either way the upstream cost stays at one request per interval.

## Staleness and failure

Polling makes failure handling simple, but you still need to handle it:

- **Mark stale data.** If `fetchedAt` is more than a few intervals old, show a warning or disable checkout in foreign currencies.
- **Back off on errors.** On repeated failures, poll less often instead of more.
- **Do not retry 401.** A bad key will not fix itself. 401, 403 and 429 responses are not charged, but they will not return data either.

## When this design is not enough

If you are building a trading screen, polling a one-minute mid-market snapshot is the wrong tool. You need bid and ask prices and a stream from a broker or a market data vendor. The [real-time forex API roundup](https://allratestoday.com/blog/best-realtime-forex-api-2026/) covers that space, and the [free currency API comparison](https://allratestoday.com/blog/best-free-currency-exchange-api-2026/) covers free snapshot APIs.

For everything else, one REST call a minute and your own SSE endpoint give every user a live rate with very little code.

## FAQ

### Does AllRatesToday offer a WebSocket feed?

No. It is REST over HTTPS only. Each rate is a one-minute mid-market snapshot, refreshed every 60 seconds. If you need push updates in your app, poll on your server and push to your clients with your own SSE or WebSocket endpoint.

### How often should I poll a live FX rates API?

No faster than the provider refreshes. If rates update every 60 seconds, polling every 60 seconds gets every update. For most pricing pages every 5 to 15 minutes is enough.

### When do I actually need WebSocket streaming for FX?

When you trade or show a trading screen: order entry, tick charts, or anything where a price a few seconds old is a problem. For that you need a broker or market data feed with bid and ask prices.

### How many requests does polling every minute use?

About 1,440 a day, or about 43,200 in a 30-day month, if one call covers all your currencies. Poll every 5 minutes and that drops to about 8,640 a month.

### Should browsers call the rates API directly?

No. Each visitor would use your quota and could see your key. Poll from your server, cache the result, and serve it to browsers from your own endpoint.

## Live mid-market rates over REST

160+ currencies refreshed every 60 seconds on every plan. One call returns as many targets as you need and counts as one request.

 [Get a free API key](https://allratestoday.com/register)

## Related Articles

- [How to cache exchange rates and avoid rate limits](https://allratestoday.com/blog/how-to-cache-exchange-rates-avoid-rate-limits/)
- [Best real-time forex APIs](https://allratestoday.com/blog/best-realtime-forex-api-2026/)
- [Best exchange rate API for Node.js](https://allratestoday.com/blog/best-exchange-rate-api-for-nodejs-2026/)
- [How often are exchange rates updated](https://allratestoday.com/blog/how-often-are-exchange-rates-updated/)
- [Real-time exchange rate API](https://allratestoday.com/real-time-exchange-rate-api/)
- [Free currency API comparison](https://allratestoday.com/blog/best-free-currency-exchange-api-2026/)
