Real-Time News Alerts with WebSockets (Socket.IO 2026)

Kent Hudson

Kent Hudson

·

18 mins read

Real-Time News Alerts with WebSockets (Socket.IO 2026)

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:

CriterionNative WebSocketSocket.IO 4.8Server-Sent Events
Latency overhead~0 ms~2–8 ms (abstraction layer)comparable to WS
DirectionBidirectionalBidirectionalServer → client only
ReconnectManual (you write it)Built-in, configurable backoffBuilt-in (browser EventSource)
Rooms / fan-out helpersNone (DIY)Native (io.to(room).emit)None
HTTP/2 multiplexingNo (uses Upgrade)NoYes — multiple streams over one connection
Firewall / proxy friendlinessLow (some corp proxies block)High (auto-fallback to long-poll)Very high (plain HTTP)
Best forCustom protocols, games, low-latency tradingNotifications, news, dashboards with subscribePure 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

mkdir news-alerts && cd news-alerts
npm init -y
npm install [email protected] 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.

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).

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.

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

<!DOCTYPE html>
<html>
<head><title>News alerts</title></head>
<body>
<ul id="feed"></ul>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io({
  reconnection: true,
  reconnectionDelay: 500,
  reconnectionDelayMax: 10_000,
  randomizationFactor: 0.5,
});

const seen = new Set();   // client-side dedup across reconnects
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:

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:

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:

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:

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

Related guides:

APITube - News API

Related articles

How to Build a News Aggregator App in 30 Minutes (2026)
Developer Guides

How to Build a News Aggregator App in 30 Minutes (2026)

Learn how to build a news aggregator app in 30 minutes with vanilla JavaScript. Search, filters, rate-limit handling — full working code included.

Next.js Multilingual App Tutorial 2026: UI + Content i18n
Developer Guides

Next.js Multilingual App Tutorial 2026: UI + Content i18n

Build a multilingual Next.js 16 app: next-intl for UI strings, APITube for content in 60+ languages, working hreflang and a multilingual sitemap. Server Components, RTL, and Vercel Edge deploy — free tier included.

10 News API Filter Patterns You Should Know (2026)
Developer Guides

10 News API Filter Patterns You Should Know (2026)

10 runnable news API filter patterns — title search, dates, entities, sentiment, sources, and compound queries — with curl, Python, and JSON examples plus the gotcha that wastes your first day.

News API Quick Start: Your First Request in 5 Minutes
Developer Guides

News API Quick Start: Your First Request in 5 Minutes

Hit a real news API endpoint in 5 minutes — curl, JavaScript, and Python side by side, with an annotated JSON response and fixes for the 401/429 errors you'll actually hit.

We use cookies

By clicking "Accept", you agree to the storing of cookies on your device for functional and analytics.