Reproducible

Methodology

Anyone should be able to check these numbers, so here is exactly what was done.

What was measured

For each feed we record, for every item that appears after we start watching:

Procedure

  1. Poll every feed once to build a baseline set of item ids. Those items are recorded but excluded from all latency statistics — they existed before we were watching, so their lag is meaningless.
  2. Re-poll every feed on a fixed 15-second cycle for the measurement window (2026-08-13 16:46–16:59 UTC, 13 minutes).
  3. Any id not previously seen is a new item: record its lag.
  4. Report the median and p90 of each feed's lag distribution, its item rate, its median HTTP response time, and its poll error count.

Known limits — stated because they change how you read the table

The collector

Standard library only, no dependencies. Point FEEDS at any RSS/Atom URLs you care about and run it.

import json, time, urllib.request, email.utils, datetime
import xml.etree.ElementTree as ET

FEEDS = {"CNBC Markets": "https://www.cnbc.com/id/10000664/device/rss/rss.html",
         "SEC EDGAR 8-K": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany"
                          "&type=8-K&count=40&output=atom"}   # add your own

def parse_dt(s):
    try:
        d = email.utils.parsedate_to_datetime(s)
    except Exception:
        d = datetime.datetime.fromisoformat(s.replace("Z", "+00:00"))
    if d.tzinfo is None:
        d = d.replace(tzinfo=datetime.timezone.utc)
    return d.timestamp()

def items(xml):
    for it in ET.fromstring(xml).iter():
        if it.tag.split("}")[-1] not in ("item", "entry"):
            continue
        d = {}
        for ch in it:
            t = ch.tag.split("}")[-1]
            if t == "link" and ch.get("href"):
                d.setdefault("link", ch.get("href"))
            elif ch.text:
                d.setdefault(t, ch.text.strip())
        uid = d.get("guid") or d.get("id") or d.get("link")
        stamp = d.get("pubDate") or d.get("published") or d.get("updated")
        if uid:
            yield uid, (parse_dt(stamp) if stamp else None)

seen, first = {k: set() for k in FEEDS}, {k: True for k in FEEDS}
end = time.time() + 900                      # 15-minute window
while time.time() < end:
    for name, url in FEEDS.items():
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "latency-check/1.0"})
            xml, now = urllib.request.urlopen(req, timeout=12).read(), time.time()
        except Exception:
            continue
        for uid, pub in items(xml):
            if uid in seen[name]:
                continue
            seen[name].add(uid)
            if not first[name] and pub:      # baseline poll is excluded
                print(json.dumps({"feed": name, "lag_s": round(now - pub, 1)}))
        first[name] = False
    time.sleep(15)

Download our raw measurements (CSV)

Measuring your feed is step one. Widening it is step two.

The feeds above are what a retail trader gets for free: one publisher each, polled on the publisher's schedule. If you want the other end of the curve — 40+ sources merged into a single stream with sentiment tagging and sub-2-second latency — that is what Top Tier Newswire is built for. The live feed is free to try; the real-time tier is paid.

Try the live feed →