Feed liveChannel BlogSports 12
Transport MQTT / SSE / RESTPush 30–150 msV1
Guides

SSE vs WebSocket Odds: Which Feed Should You Use?

SSE vs WebSocket odds feeds compared by a founding engineer: unidirectional push with auto-reconnect vs a full-duplex raw stream, and when REST polling wins.

Short version: for consuming a real-time odds feed, SSE and WebSocket are not competitors so much as two answers to different questions. SSE is a one-way push channel with auto-reconnect built in and dead-simple ?key= auth — perfect for a filtered signal like drop alerts. A WebSocket is a full-duplex socket you drive yourself — the right call when you want the raw firehose of every market transition. And sometimes neither wins: polling REST is the honest answer.

The sse vs websocket odds question usually shows up when someone has decided they want "real-time" and assumes that means WebSockets. It often doesn't. Let me walk through what each protocol actually gives you, where each fits an odds feed, and how pinnodds uses both — because we ship SSE and a raw socket on purpose.

The protocols, without the hand-waving

Both SSE and WebSocket keep a connection open so the server can push to you instead of you polling. That is the only thing they have in common. The differences are the whole story.

Server-Sent Events (SSE) is one-way. The server sends you a stream of UTF-8 text events over an ordinary long-lived HTTP response; you never send anything back on that channel. Because it rides plain HTTP, it goes through proxies and load balancers without special handling, and the client — EventSource in the browser, or a library elsewhere — reconnects automatically when the connection drops, resuming from the last event it saw. You pay for that simplicity with a constraint: text only, one direction, and a browser cap of six concurrent connections per origin over HTTP/1.1.

WebSocket is full-duplex. After an HTTP upgrade handshake, you have a persistent bidirectional socket carrying text or binary frames in either direction. That power is the cost: nothing reconnects for you, nothing resumes for you, and you are responsible for heartbeats, backpressure, and the state machine that turns a stream of frames into a coherent picture. There is no ?Last-Event-ID equivalent unless the application layer builds one.

For an odds feed, the decisive question is almost never "do I need to send data back" (you rarely do). It is "do I want a filtered signal, or every transition?"

Where SSE fits an odds feed

If your product is "tell me when a price falls against its own recent history," SSE is the correct tool and you should stop looking at WebSockets. pinnodds serves drop alerts as Server-Sent Events at /odds-drop (live) and /odds-drop-prematch (prematch). Each alert is self-contained, so you hold no state — the whole client is a few lines:

import EventSource from "eventsource";

const url = `https://pinnodds.com/odds-drop?key=${process.env.PINNODDS_KEY}`;
const es = new EventSource(url);

es.onmessage = (e) => {
  const d = JSON.parse(e.data);
  // sect = market, outcome = moving selection
  // limit = Pinnacle max stake, nvp = no-vig fair price
  console.log(
    `${d.id} ${d.sect} ${d.outcome}: ${d.from_price} -> ${d.to_price} (nvp ${d.nvp})`
  );
};

// Auto-reconnect is built in. This fires on a dropped connection;
// EventSource retries on its own — you don't reopen it.
es.onerror = (err) => console.warn("sse hiccup, will auto-retry", err?.message);

Three things are doing real work here and are worth naming:

  • Auth is a query param. ?key=YOUR_KEY on the URL and you are streaming. (REST uses the x-api-key header; the streams use ?key= because you can't set headers on a browser EventSource.) A free trial key takes seconds and no card.
  • Reconnect is free. EventSource reopens on its own after a drop and picks the stream back up. You do not write retry loops or exponential backoff.
  • State is zero. The alert names the market in sect, the moving selection in outcome, the prices in from_price/to_price, the event in id, and carries limit (Pinnacle's max stake) and nvp (the no-vig fair price). Nothing needs to be reconciled against a prior snapshot.

One shape detail trips people up: the SSE alert carries no drop percentage. The precomputed drop_pct lives on the REST side at /api/drops (mode=live|prematch), which also renames things — market, designation, from/to, event_id. Treat the SSE alert and the REST row as two views of one event, keyed on id / event_id. Full field reference is in the docs.

Where the WebSocket fits

The SSE channel is a filter. It gives you one category of event — a price falling — and throws the rest away. That is exactly what you want most of the time and exactly what you don't want some of the time.

/ws/feed is a raw WebSocket passthrough of the Pinnacle stream: every message Pinnacle emits, in order, unfiltered and unenriched. Reach for it when:

  • You want every transition, not a category of transition. Rises, reversals, oscillation around a key number — a drop filter never shows you those.
  • You are building your own signal. Steam, cross-market divergence, limit changes as a confidence proxy, quarter-line spreads: all of it needs the raw material.
  • You are recording history. There is no historical odds archive here (more on that below) — if you want a time series you build it from the socket, starting the day you connect.
  • You fan out to many consumers behind one connection. One socket into your own internal bus beats every service polling REST.

A minimal client, with the reconnect logic SSE would have given you for free:

import WebSocket from "ws";

function connect() {
  const ws = new WebSocket(`wss://pinnodds.com/ws/feed?key=${process.env.PINNODDS_KEY}`);

  ws.on("open", () => console.log("feed open"));

  ws.on("message", (buf) => {
    const msg = JSON.parse(buf.toString());
    // Passthrough: upstream shapes, no drop_pct, no normalisation.
    // You are the database — apply this mutation onto a snapshot
    // you seeded from REST (/kit/v1/markets or /kit/v1/prematch/lines).
    applyMutation(msg);
  });

  // You own reconnect AND the re-seed. A gap means your in-memory
  // book is a guess until you refetch the snapshot.
  ws.on("close", () => {
    setTimeout(() => { reseedFromRest(); connect(); }, 1000);
  });

  ws.on("error", (e) => { console.warn("ws error", e.message); ws.close(); });
}

connect();

Notice what changed versus the SSE example: you write the reconnect, you write the re-seed, and the comment // You are the database is the entire burden in four words. A socket hands you a sequence of mutations, not a book. Get the snapshot ordering wrong — apply a stale REST seed over fresh socket updates — and you have a phantom edge that surfaces three hours later.

Reconnection and backpressure, side by side

This is where the abstractions stop being equivalent.

Reconnection. SSE reconnects itself and can resume from the last event id. A WebSocket does neither — every disconnect is a gap, and a gap in an odds book is a correctness bug, not a latency blip. If you take the socket, the reconnect handler that re-seeds from REST is not optional polish; it is the first thing you write, before the message handler.

Backpressure. On a raw socket you can out-run your own consumer. Turn on include_specials=1 and soccer prematch goes from roughly 1,500 events to roughly 12,400 — player and team props, exact scores, futures and outrights, each arriving as its own event row with special, special_category, special_markets and a parent_id. That is a legitimate firehose, and if your handler blocks, frames queue in memory until something falls over. SSE sidesteps this because the drop filter has already thinned the stream to the handful of events that matter.

Ordering and completeness. The socket guarantees you see transitions in order. SSE guarantees you see drops, self-contained, with no ordering obligations on your side. Pick the guarantee your product actually needs.

Side by side

SSE (/odds-drop)WebSocket (/ws/feed)REST poll (/api/drops)
DirectionServer → client onlyFull-duplexRequest/response
Auth?key= in URL?key= in URLx-api-key header
ReconnectAutomatic, resumesYou write itN/A (stateless)
State you holdNoneThe whole bookNone
PayloadFiltered drop alertsEvery transition, rawEnriched drops w/ drop_pct
Backpressure riskLow (pre-filtered)Real (you manage it)None
Best forDrop alerting, push signalsFull stream, own signals, recordingTimers, serverless, ranked views
PlanPro + SSE $149/mo+Scale $229/moPro (REST) $99/mo

When polling REST is actually the right call

Streaming is not automatically better, and pretending otherwise wastes people's time. Poll GET /api/drops or /kit/v1/markets when:

  • Your consumer is a UI on a timer. A dashboard refreshing every 30 seconds gains nothing from a persistent connection and inherits a reconnect surface for free. Just poll.
  • You have no warm process. Serverless functions, cron jobs, edge workers — anything that spins up, does one thing, and dies — cannot hold a stream. REST is the match.
  • You want the enriched, ranked view. /api/drops hands you drop_pct already computed. If you want "the biggest drops right now" as a sortable list rather than a live trigger, that is a query, not a subscription.
  • You are still learning the vocabulary. Build against REST first with a debugger before you open any stream. It is far easier to understand market / designation / from / to from a static response than from a moving one.
// Perfectly good for a timer-driven consumer — no connection to babysit.
const res = await fetch("https://pinnodds.com/api/drops?mode=live", {
  headers: { "x-api-key": process.env.PINNODDS_KEY },
});
const drops = await res.json();
for (const d of drops) {
  if (d.drop_pct >= 4) console.log(d.event_id, d.market, d.designation, d.drop_pct);
}

The honest failure mode of polling is that it hides transitions: a line that goes 2.10 → 1.95 → 2.05 inside your interval looks like it never moved. If that matters to you, stream. If it doesn't — if you only need a periodic snapshot of the current state — polling is less code and fewer bugs.

How to pick

Walk it in this order and you won't repaint:

  1. Can you phrase your need as "notify me when a price falls"? Use SSE. /odds-drop or /odds-drop-prematch, auto-reconnect, zero state. This covers most teams.
  2. Can you phrase it as a question with an answer at a point in time? Use REST. /api/drops for ranked drops, /kit/v1/markets for the board. No connection to manage.
  3. Do you need every transition, your own signal, or a recording of the feed? Use the WebSocket — and write the re-seed-on-reconnect path before the message handler.

Both official SDKs — npm install pinnodds and pip install pinnodds — are zero or low dependency and wrap the REST and SSE surface, so you only hand-roll a client if you are on the raw socket, where you were going to write one anyway.

If you want the deeper protocol notes on the raw feed specifically, the Pinnacle WebSocket API page covers the passthrough contract, and the main product page has the full endpoint map. Pricing — REST on Pro, SSE on Pro + SSE, both on Scale — is on the pricing section.

Where none of this is the right tool

Being specific, because vagueness costs people a month:

  • You need many books. pinnodds is Pinnacle only. No protocol changes that — if your product compares 40 bookmakers or runs arbitrage, you want an aggregator, and you layer pinnodds alongside it as the reference price.
  • You need deep history over the wire. There is no historical odds archive; SSE, WebSocket and REST are all real-time or recent-view surfaces. Record /ws/feed yourself from day one, or buy history elsewhere.
  • You want a stable, owned contract but choose the socket. /ws/feed is a passthrough — its shapes follow upstream. If you need a shape pinnodds owns and versions, that is the REST surface, not the raw feed.

Takeaway

The cleanest mental model: SSE is a push channel for a signal you can name, a WebSocket is a mutation log for everything, and REST is a question with an answer. Most odds consumers want the signal — so start with SSE, keep your process stateless, and reconcile against /api/drops when you want the drop_pct. Open the raw socket only when you genuinely need every transition, and when you do, seed from /kit/v1/markets or /kit/v1/prematch/lines on every single reconnect. Pick the protocol that matches the shape of your question, not the one that sounds the most real-time.

Frequently asked questions

What is the difference between SSE and WebSocket for an odds feed?

SSE is a one-way channel: the server pushes text events to you over a plain HTTP connection, and the browser or client reconnects automatically if it drops. A WebSocket is a full-duplex socket — you can send as well as receive — but you own the reconnect logic and the state machine. For odds you rarely need to talk back, so SSE fits a filtered signal like drop alerts, while a WebSocket fits the raw firehose of every market transition.

Does pinnodds use SSE or WebSocket?

Both. Server-Sent Events at /odds-drop (live) and /odds-drop-prematch push self-contained drop alerts with no state to hold. The raw WebSocket passthrough at /ws/feed carries every message Pinnacle emits, in order, for teams that want the full market stream. You pick per use case; many teams use SSE and never open the socket.

How do I authenticate to an SSE or WebSocket odds feed?

pinnodds uses one API key for everything. On the streams — both the SSE endpoints and /ws/feed — pass it in the URL as ?key=YOUR_KEY. On the REST endpoints send it as the header x-api-key: YOUR_KEY. The same key is also your login.

Is polling REST ever better than streaming odds?

Yes. If your consumer is a dashboard on a timer, a serverless function, a cron job, or anything without a warm long-lived process, polling GET /api/drops or /kit/v1/markets is simpler and has no reconnect surface. Streaming only pays off when you hold a warm connection and care about transitions between poll windows.

Why does the SSE odds-drop alert have no drop percentage?

The SSE payload is the raw alert: sect, outcome, from_price, to_price, id, plus limit and nvp. The precomputed drop_pct lives on the REST row at /api/drops (mode=live or mode=prematch), which also renames the fields to market, designation, from/to and event_id. Treat the two as one event keyed on id/event_id.

Do I need a WebSocket to catch odds drops?

No. Drop alerts are a filtered category of transition, and SSE delivers exactly that with no state to maintain. Reach for the WebSocket only when you want every transition — rises, reversals, oscillation around a key number — not just the ones a drop filter selects.

Get real-time Pinnacle odds in your code

The Pinnacle APIby pinnodds: live & prematch markets with instant odds-drop alerts over SSE and WebSocket. See plans and pricing, or get a free key in seconds — no card, no Pinnacle account.

Get your free API key