---
title: "Real-Time News Alerts with WebSockets (Socket.IO 2026)"
description: "Build real-time news alerts with WebSockets in Node.js using Socket.IO 4.8 and APITube SSE. Room filters, reconnect, Redis adapter, full code."
source: https://apitube.io/blog/post/real-time-news-alerts-websockets
---

# Real-Time News Alerts with WebSockets: Node.js + Socket.IO Tutorial

**A real-time news alert system is a server-to-browser pipeline that streams news articles the moment they're published, filtered per user, over a persistent connection — typically a WebSocket.** In 2026 the right stack for this is Socket.IO 4.8 over Node.js, an upstream SSE feed (we'll use APITube), and one trick most tutorials miss: rooms-as-filters, where each user joins `entity:Tesla` or `sentiment:negative` rooms and the server fans out only matching articles.

Architecture, end to end:

```
APITube SSE  →  Node.js relay  →  Socket.IO rooms  →  Browser  →  user UI
(/v1/news/stream)  (subscribe + match)  (entity:X, sentiment:Y)  (per-user filters)
```

> **Key takeaways**
> - Use Socket.IO rooms named `entity:X` and `sentiment:Y` to fan out filtered articles without an N×M loop on every emit.
> - Don't fake events with `setInterval` — connect the relay to a real upstream like APITube's `/v1/news/stream` SSE endpoint.
> - For news-style server push, SSE alone would do the job — pick Socket.IO when you need bidirectional subscribe + presence + room patterns.
> - Production hardening matters more than reconnection logic: dedup with a capped `Set`, drop on slow clients, scale horizontally with `@socket.io/redis-adapter`.

This tutorial walks through a deployable Node.js relay in about 100 lines: it subscribes to APITube's SSE feed, matches each article to user-defined filters, fans out via Socket.IO rooms, dedupes, reconnects, and runs across multiple instances behind Redis.

## What you'll build

- Node.js relay that consumes APITube's SSE news stream as upstream
- Socket.IO server that lets browsers subscribe to per-user filters
- Browser client that joins `entity:*` and `sentiment:*` rooms and renders matching articles in real time
- Reconnect with exponential backoff, dedup by article ID, backpressure on slow clients
- Redis adapter for horizontal scaling across Node instances

## WebSocket vs Socket.IO vs SSE — what to use for news

The PAA top question is "what's the difference?" Numbers, not opinions:

| Criterion | Native WebSocket | Socket.IO 4.8 | Server-Sent Events |
|---|---|---|---|
| Latency overhead | ~0 ms | ~2–8 ms (abstraction layer) | comparable to WS |
| Direction | Bidirectional | Bidirectional | Server → client only |
| Reconnect | Manual (you write it) | Built-in, configurable backoff | Built-in (browser EventSource) |
| Rooms / fan-out helpers | None (DIY) | Native (`io.to(room).emit`) | None |
| HTTP/2 multiplexing | No (uses Upgrade) | No | Yes — multiple streams over one connection |
| Firewall / proxy friendliness | Low (some corp proxies block) | High (auto-fallback to long-poll) | Very high (plain HTTP) |
| Best for | Custom protocols, games, low-latency trading | Notifications, news, dashboards with subscribe | Pure server-push (logs, dashboards, telemetry) |

**Unlike native WebSocket, Socket.IO ships rooms and fan-out helpers out of the box**, which means a news bot's "match article → emit to N rooms" loop is one line instead of a manual subscriber registry. We'll use Socket.IO for the bidirectional `subscribe` event + room fan-out; the upstream APITube feed is SSE because it only flows one way.

One contrarian aside on managed alternatives: Pusher, Ably, and Novu charge per concurrent connection plus per message.

> At 1,000 articles fanned out to 10,000 subscribers, that's 10 million deliveries a day. On Pusher's per-message pricing this is hundreds of dollars per month; the same load runs on a $20/month VPS with Node and Redis. Per-message billing is great for transactional notifications and a tax on news fan-out.

## Prerequisites

- Node.js 20 LTS or newer (22 recommended for native `EventSource`)
- A free APITube API key from apitube.io — the SSE stream is on the free plan (disclosure: I work on APITube)
- Basic familiarity with `async`/`await`, Express or any Node HTTP server, and Socket.IO concepts

## How to build real-time news alerts with WebSockets (6 steps)

1. Set up the Node.js project
2. Create the Socket.IO server with subscribe handler
3. Connect the relay to APITube's SSE upstream
4. Match each article to rooms and emit
5. Build the browser client with subscribe + reconnect + dedup
6. Add production hardening (Redis, dedup cap, backpressure)

### Step 1 — Set up the project

```bash
mkdir news-alerts && cd news-alerts
npm init -y
npm install socket.io@4.8 eventsource@2 express@4
npm install @socket.io/redis-adapter@8 redis@4   # for step 6
```

`.env`:

```
APITUBE_KEY=your_apitube_key
PORT=3000
REDIS_URL=redis://localhost:6379
```

### Step 2 — Socket.IO server with subscribe handler

When a browser connects, it sends a `subscribe` event listing the entities and sentiments it cares about. The server joins the corresponding rooms — that's the entire subscription model.

```js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
app.use(express.static('public'));
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: '*' } });

io.on('connection', (socket) => {
  socket.on('subscribe', ({ entities = [], sentiments = [] }) => {
    entities.forEach(e => socket.join(`entity:${e}`));
    sentiments.forEach(s => socket.join(`sentiment:${s}`));
    socket.emit('subscribed', { entities, sentiments });
  });
});

httpServer.listen(process.env.PORT, () =>
  console.log(`up on :${process.env.PORT}`)
);
```

### Step 3 — Connect the relay to APITube's SSE upstream

Node 22 has native `EventSource`, but the `eventsource` lib remains the safer bet because it lets you set custom headers (the `X-API-Key` auth header isn't supported by the spec'd browser `EventSource`).

```js
const EventSource = require('eventsource');

const upstream = new EventSource('https://api.apitube.io/v1/news/stream', {
  headers: { 'X-API-Key': process.env.APITUBE_KEY },
});

upstream.onopen = () => console.log('upstream connected');
upstream.onerror = (err) => console.error('upstream error', err);
upstream.onmessage = (ev) => {
  const article = JSON.parse(ev.data);
  routeArticle(article);   // step 4
};
```

If you're on the free plan and the SSE endpoint is rate-limited for your tier, fall back to polling `/v1/news/everything` every 60 seconds with `published_at.start` set to the last-seen timestamp.

### Step 4 — Match articles to rooms and emit

This is the step that makes the system actually a news alert system. Each article carries an `entities[]` array and a `sentiment.overall.polarity`. The server emits the same article to every relevant room.

```js
const seen = new Set();   // dedup; capped in step 6

function routeArticle(article) {
  if (seen.has(article.id)) return;
  seen.add(article.id);

  const polarity = article.sentiment?.overall?.polarity;
  const entityNames = (article.entities || []).map(e => e.name);

  // emit to entity rooms
  for (const name of entityNames) {
    io.to(`entity:${name}`).emit('article', article);
  }
  // emit to sentiment room
  if (polarity) {
    io.to(`sentiment:${polarity}`).emit('article', article);
  }
}
```

Socket.IO's room emit is internally a hash lookup — O(rooms_targeted), not O(connected_clients). A single article mentioning Tesla and SpaceX with negative sentiment fans out to three rooms in microseconds, regardless of whether 100 or 100,000 clients are subscribed.

> **Rooms-as-filters** is a Socket.IO pattern where each filter dimension (entity name, sentiment polarity, category) becomes a room name. The server matches each incoming event to the relevant rooms and emits once per room, replacing per-client `WHERE` filtering with O(rooms_targeted) fan-out.

### Step 5 — Browser client: subscribe, reconnect, dedup

```html
<!DOCTYPE html>
<html>
<head>
<link rel="dns-prefetch" href="//apitube.io">
<link rel="dns-prefetch" href="//api.apitube.io">
<link rel="dns-prefetch" href="//docs.apitube.io"><title>News alerts</title></head>
<body>
<ul id="feed"></ul>
<script  src="/socket.io/socket.io.js" defer></script>
<script>
const socket = io({
  reconnection: true,
  reconnectionDelay: 500,
  reconnectionDelayMax: 10_000,
  randomizationFactor: 0.5,
});

const seen = new Set();   
const feed = document.getElementById('feed');

socket.on('connect', () => {
  socket.emit('subscribe', {
    entities: ['Tesla', 'SpaceX', 'OpenAI'],
    sentiments: ['negative'],
  });
});

socket.on('article', (a) => {
  if (seen.has(a.id)) return;
  seen.add(a.id);
  const li = document.createElement('li');
  li.innerHTML = `<a href="${a.href}" target="_blank">${a.title}</a>
    <small>· ${a.source.domain} · ${a.sentiment.overall.polarity}</small>`;
  feed.prepend(li);
});

socket.on('disconnect', () => console.warn('disconnected, reconnecting…'));
</script>
</body>
</html>
```

Socket.IO's client reconnect uses exponential backoff with jitter by default; the config above just tightens the bounds for a news UX (start at 500 ms, cap at 10 s).

The client-side `seen` Set is what protects against duplicates that survive a reconnect — the server may legitimately re-emit during a brief disconnect, and you'd rather drop a dup than render the same headline twice.

### Step 6 — Production hardening

Three things separate a demo from a deployable relay.

**Cap the dedup Set** so it doesn't grow unbounded:

```js
const SEEN_MAX = 5000;
const seen = new Set();

function markSeen(id) {
  if (seen.size >= SEEN_MAX) {
    const first = seen.values().next().value;
    seen.delete(first);
  }
  seen.add(id);
}
```

`Set` preserves insertion order, so the first key is always the oldest. Cheap LRU.

**Backpressure on slow clients** — if a socket's send buffer is large, drop the article rather than queueing more:

```js
function emitWithBackpressure(room, payload) {
  for (const sid of io.of('/').adapter.rooms.get(room) || []) {
    const s = io.sockets.sockets.get(sid);
    if (s && s.conn.transport.writable) {
      s.emit('article', payload);
    }
  }
}
```

**Redis adapter for horizontal scale** — multiple Node instances behind a sticky load balancer all see the same `io.to(room).emit`:

```js
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
```

Stick the upstream SSE consumer on one designated instance (or a separate worker) so you don't connect to APITube N times — every Node process consuming once is fine for a small fleet, but it's wasteful and risks rate limits.

## Complete code

`server.js`:

```js
require('dotenv').config();
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const EventSource = require('eventsource');

const SEEN_MAX = 5000;
const seen = new Set();
const markSeen = (id) => {
  if (seen.size >= SEEN_MAX) seen.delete(seen.values().next().value);
  seen.add(id);
};

async function main() {
  const app = express();
  app.use(express.static('public'));
  const httpServer = createServer(app);
  const io = new Server(httpServer, { cors: { origin: '*' } });

  if (process.env.REDIS_URL) {
    const pub = createClient({ url: process.env.REDIS_URL });
    const sub = pub.duplicate();
    await Promise.all([pub.connect(), sub.connect()]);
    io.adapter(createAdapter(pub, sub));
  }

  io.on('connection', (socket) => {
    socket.on('subscribe', ({ entities = [], sentiments = [] }) => {
      entities.forEach(e => socket.join(`entity:${e}`));
      sentiments.forEach(s => socket.join(`sentiment:${s}`));
      socket.emit('subscribed', { entities, sentiments });
    });
  });

  const upstream = new EventSource('https://api.apitube.io/v1/news/stream', {
    headers: { 'X-API-Key': process.env.APITUBE_KEY },
  });

  upstream.onmessage = (ev) => {
    const a = JSON.parse(ev.data);
    if (seen.has(a.id)) return;
    markSeen(a.id);

    (a.entities || []).forEach(e =>
      io.to(`entity:${e.name}`).emit('article', a)
    );
    const p = a.sentiment?.overall?.polarity;
    if (p) io.to(`sentiment:${p}`).emit('article', a);
  };

  upstream.onerror = (err) => console.error('upstream', err);

  process.on('SIGTERM', () => {
    upstream.close();
    io.close();
    httpServer.close();
  });

  httpServer.listen(process.env.PORT || 3000);
}

main().catch(console.error);
```

The browser file is `public/index.html` from step 5.

## Production checklist

- Pin Node to 20 LTS or 22 LTS (`engines` in `package.json`)
- `.env` for `APITUBE_KEY`, `REDIS_URL`, `PORT` — never commit
- Sticky sessions on the load balancer (Socket.IO requires the same client to hit the same Node process during the handshake)
- Cap the dedup `Set` (5,000 IDs ≈ a few hours of news at 1k articles/day)
- Drop on slow clients instead of buffering — memory beats fairness here
- Capture `disconnect` reasons in logs; an uptick in `transport close` usually means a proxy is timing your connections out
- One upstream SSE consumer per Node cluster, not per instance — split the role into a worker process
- Graceful shutdown: close the upstream EventSource, then `io.close()`, then HTTP server

## FAQ

### What is the difference between WebSocket and Socket.IO?

WebSocket is the raw bidirectional protocol baked into browsers. Socket.IO is a Node.js library that uses WebSocket under the hood and adds rooms, automatic reconnect with backoff, namespaces, and HTTP long-polling fallback. For real-time news alerts you can ship raw WebSocket, but Socket.IO's room API is what makes per-user filter fan-out a one-liner.

### How do you push real-time notifications to a website?

Open a persistent connection from the browser to your server — typically a WebSocket via Socket.IO — and have the server `emit` events whenever a notification fires. The browser listens with `socket.on('event', handler)` and updates the UI. For news alerts specifically, the server must also subscribe to an upstream feed (SSE, webhook, or message queue) so emitted events reflect real publications, because synthetic `setInterval` events don't survive contact with a production deployment.

### Are WebSockets still relevant in 2026?

Yes. WebSockets remain the default for real-time browser apps in 2026 — chat, live dashboards, multiplayer, and notification systems. SSE has gained ground for pure server-push workloads thanks to HTTP/2 multiplexing, but WebSockets keep the bidirectional + low-latency niche, and library ecosystems (Socket.IO 4.8, ws, uWebSockets) are healthy and actively maintained.

### Can WebSockets be used for news feeds?

Yes — WebSockets are well-suited to news feeds because the workload (server pushes new articles, client occasionally subscribes/unsubscribes) maps cleanly onto the protocol's bidirectional model. Pair WebSockets with Socket.IO rooms to filter by entity, sentiment, or category, and dedup on both server and client to handle reconnects without duplicates.

## Try it

Clone the snippets, fill `.env`, `node server.js`, open `index.html`. APITube's free tier includes the SSE stream at `/v1/news/stream` — get a key at apitube.io and replace the placeholder. Browser shows live articles whenever the upstream feed fires an event matching the filters you subscribed to.

## Resources

- **APITube** — [apitube.io](https://apitube.io) — try it free, sentiment and entities included on every article
- **Documentation** — [docs.apitube.io](https://docs.apitube.io) — endpoints, parameters, response structure, integrations
- **Pricing** — [apitube.io/pricing](https://apitube.io/pricing) — all tiers
- **APITube blog** — [apitube.io/blog](https://apitube.io/blog) — more guides and comparisons

**Related guides:**
- [Best News API for JavaScript & Node.js (2026)](https://apitube.io/blog/best-news-api-javascript-nodejs-2026)
- [Best News API for React (2026)](https://apitube.io/blog/best-news-api-react-2026)
- [Building a Telegram News Bot with Python](https://apitube.io/blog/telegram-news-bot-python)
