HomeBlog › How We Measure Pinnacle API Latency (and How to Reproduce It)
Guides

How We Measure Pinnacle API Latency (and How to Reproduce It)

How We Measure Pinnacle API Latency (and How to Reproduce It)

The methodology behind our 15–40 ms latency figure: what we measure, how, the full distribution — and a harness you can run yourself.

How We Measure Pinnacle API Latency (and How to Reproduce It)

We quote "roughly 15–40 ms" for end-to-end latency in a lot of places. A number like that is worthless unless you can see how it was produced and reproduce it yourself, so this post is the methodology behind it — including the parts that make the number look less tidy than a marketing range.

The short version: latency is a distribution, not a single figure, and the honest way to report it is with percentiles and the conditions they were measured under. Here's all of it.

What "latency" even means here

People mean different things by "fast," so let's be precise. We measure frame-to-client latency: the time from when a price change appears at our ingestion edge (the "frame") to when the corresponding event is fully received and parsed by a connected client.

That deliberately excludes one thing and includes another:

If a vendor quotes a smaller number, the first question is always: measured between which two points? A figure measured "server to server in the same datacenter" is not comparable to one measured "to a client across the public internet."

The measurement setup

Each frame carries a monotonic ingestion timestamp, t_ingest. The client stamps t_recv the moment it finishes parsing the event. Latency for that event is t_recv - t_ingest.

The honest complication is clock skew: t_ingest and t_recv come from different machines, whose clocks drift. We handle it two ways, and you should too:

  1. Same-host loopback run — run the client on the same machine as a co-located probe so both timestamps share one clock. This removes skew entirely and isolates our processing + fan-out.
  2. Cross-region run with NTP discipline — for realistic transit numbers, run the client where your app actually lives, with NTP synced, and treat the absolute number as approximate (±a few ms of skew) while trusting the shape of the distribution.

We report both, because they answer different questions: the loopback run is "how fast is the service," the cross-region run is "how fast will it be for me."

Report the distribution, not a headline

A single number hides the tail, and the tail is where real systems get hurt. The format we're committing to — and will fill in on this page from a current, disclosed collection run rather than a marketing sample — is percentiles per run type: p50 / p95 / p99 / max-over-an-hour, reported separately for a same-host loopback run and a cross-region run. We're collecting that sample on the current infrastructure now; a table of numbers you can't yet reproduce would be exactly the kind of latency claim this post exists to criticize.

Until it lands, treat our "15–40 ms" as what it honestly is: the rough p50–p95 band we observe on cross-region runs from a nearby region — a claim to verify with the harness below, not a datasheet. Your p99 will be higher; plan for it. If a feed only ever tells you one number, they're telling you their p50 and hoping you don't ask about the tail.

Run it yourself

The whole point is that you don't have to trust the table. The harness below is self-contained — every alert on our /odds-drop stream carries alerted_ms, the millisecond wall-clock stamp from the moment the drop was detected, so the client can compute its own end-to-end figure per event:

import { EventSource } from "eventsource";

const samples = [];
const es = new EventSource(`https://pinnapi.com/odds-drop?key=${process.env.PINNAPI_KEY}&min_drop=5`);

es.onmessage = (e) => {
  const tRecv = Date.now();                  // stamp FIRST, then parse
  const payload = JSON.parse(e.data);
  if (payload.type === "connected") return;  // first frame is a handshake
  for (const alert of payload) {
    if (alert.alerted_ms) samples.push(tRecv - alert.alerted_ms);
  }
};

// every minute, print the distribution
setInterval(() => {
  const s = [...samples].sort((a, b) => a - b);
  if (!s.length) return;
  const pct = (p) => s[Math.floor((p / 100) * s.length)];
  console.log({ n: s.length, p50: pct(50), p95: pct(95), p99: pct(99), max: s[s.length - 1] });
}, 60_000);

Each alert also carries dispatched_ms — stamped just before the frame is written to the socket — so you can split the total into pipeline time (dispatched_ms - alerted_ms, inside our service) and wire time (tRecv - dispatched_ms, network plus your parse). That split is how you tell a slow feed from a slow route.

To make it a fair fight, run the same harness against any competing feed (adjust the endpoint and the field name for their emission timestamp; if they don't expose one, that itself is informative). Same client, same region, same hour — otherwise you're comparing weather, not feeds.

Things that will skew your numbers (control for them)

Why we bother publishing this

Two reasons, one principled and one self-interested. The principled one: latency claims in this space are mostly unfalsifiable marketing, and that's bad for everyone building real systems. The self-interested one: we think we do well on the cross-region tail, and the only way that claim is worth anything is if you can check it. So check it.

This post is also one half of a pair. Feed latency measures how fast a Pinnacle move reaches you; the sister methodology measures how fast the rest of the market follows Pinnacle once it moves — and it's the ratio between those two numbers that decides whether a sharp-line strategy is viable at all.

So what's the fastest odds API? Measure it

"Fast" is a distribution measured between two specified points under stated conditions. Ask any feed — including us — for percentiles, the measurement endpoints, and the conditions. If you only get a single number with no method, treat it as a p50 told in good lighting, and go measure it yourself.

Frequently asked questions

Why a range (15–40 ms) instead of one number?

Because latency varies by region, load, and percentile. The range is the rough p50–p95 cross-region band; the full table is above.

Can I reproduce your figures?

Yes — that's the whole point. The harness above is self-contained (the stream's alerted_ms field does the server-side stamping); run it from your own region. Note the drop stream is a paid capability — the free tier is REST-only, so plan the measurement into your first paid month and hold us to it.

How do you handle clock skew between server and client?

A same-host loopback run removes it for the service number; cross-region runs use NTP-synced clocks and treat absolute values as approximate while trusting the distribution's shape.

How much latency matters for arbitrage betting bots?

Enough to decide viability outright: the exploitable window opens when Pinnacle moves and closes when the soft book reprices, so your end-to-end latency has to fit inside it — the arbitrage guide has the math.

Get real-time Pinnacle odds in your code

Live & prematch markets with sub-second odds-drop alerts. Free trial key in seconds — no card.

Start free trial