> ## Documentation Index
> Fetch the complete documentation index at: https://docs.therundown.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Efficient Polling

> Optimize data-point usage with delta endpoints, tight filters, smart polling intervals, and caching strategies.

Polling the full events endpoint every few seconds is expensive, burns data points, and can trip your per-second throttle if you scale it across many sports or books. This guide covers how to keep data fresh without paying for or requesting far more than you need.

## Why Polling Efficiency Matters

TheRundown usage is not just about request count. The cost of a polling loop depends on:

* how many sportsbooks you request
* how many markets you include
* whether you pull full event payloads or only deltas
* how often you refresh

A naive loop that fetches full event snapshots every few seconds can waste both data points and burst budget. Delta endpoints solve this by returning **only what changed** since your last request.

## Shrink Every Response First

Before you tune polling intervals, make each response smaller.

```bash theme={null}
TODAY_UTC=$(date -u +%F)

curl -H "X-TheRundown-Key: $THERUNDOWN_API_KEY" \
  "https://therundown.io/api/v2/sports/3/events/$TODAY_UTC?market_ids=1,2,3,41,42,43&affiliate_ids=19,23&main_line=true"
```

Use these levers aggressively:

* `market_ids`: biggest control for response size
* `affiliate_ids`: only request the books you actually surface
* `main_line=true`: skip alternate lines if your product only shows the primary market
* event-specific endpoints: if you only care about a handful of games, do not fetch the full slate
* `hide_closed_markets=1`: useful during market discovery to avoid off-board clutter

## Delta Endpoint Bootstrap Flow

The pattern for using delta endpoints has three stages:

### 1. Fetch the full snapshot

Start by loading the complete event data for the sport and date you need. This gives you the initial state and the first delta cursor.

```bash theme={null}
TODAY_UTC=$(date -u +%F)

curl -H "X-TheRundown-Key: $THERUNDOWN_API_KEY" "https://therundown.io/api/v2/sports/3/events/$TODAY_UTC?market_ids=1,2,3,41,42,43&affiliate_ids=19,23&main_line=true"
```

For a plan with explicitly confirmed zero data delay, the response body's `meta.delta_last_id` is the canonical bootstrap cursor for **`/api/v2/markets/delta`**. Accept a positive integer whether encoded as a number or string. Never replace a missing or invalid cursor with `0`.

<Warning>
  Delayed plans use narrow, repeated event snapshots and never bootstrap market-delta polling from a delayed snapshot. A missing or invalid `X-Data-Delay-Seconds` header means the delay is unknown, not zero. Use delta polling only when a known plan entitlement establishes zero delay; a positive server-reported delay always rules out delta polling.
</Warning>

### 2. Poll the market delta endpoint

On each subsequent poll, pass your saved cursor to `/api/v2/markets/delta`. It returns only the prices that changed since that cursor — the most efficient way to keep odds fresh.

Use the same explicit full-game core scope in the snapshot and delta request: `1,2,3,41,42,43`. Market delta filters IDs literally, and delta rows retain their actual market IDs. For soccer, NHL regulation-time, tennis, and futures scopes, use the sport-aware [recommended core market scopes](/reference/sports#recommended-core-market-scopes).

```bash theme={null}
curl -H "X-TheRundown-Key: $THERUNDOWN_API_KEY" "https://therundown.io/api/v2/markets/delta?last_id=PREVIOUS_DELTA_LAST_ID&sport_id=3&market_ids=1,2,3,41,42,43&affiliate_ids=19,23"
```

Each response includes its own `meta.delta_last_id` (also an integer) — use that as `last_id` on the next poll. Do **not** bootstrap with `last_id=0`: a cursor that has fallen too far behind the current head is rejected, so always seed from a fresh, zero-delay events snapshot.

<Warning>
  The two delta endpoints take **different, non-interchangeable cursors**:

  * **`/api/v2/markets/delta`** (price changes) uses an **integer** cursor. On known zero-delay access, bootstrap it from the events snapshot's integer `meta.delta_last_id`, then follow the integer cursor in each markets-delta response. This is the endpoint for odds polling.
  * **`/api/v2/delta`** (full event-object changes: status, scores, the whole event) uses an **ordered UUID** cursor, e.g. `11f1-23b3-f4d42784-8057-a3a997572248`. That cursor is only returned by `/api/v2/delta`'s own responses — the events snapshot does **not** provide it.

  Passing the integer events cursor to `/api/v2/delta` returns a `400`. For price and odds polling, use `/api/v2/markets/delta`.
</Warning>

### 3. Merge updates into local state

Each delta response contains changed price rows. Upsert each row by its exact event, market, participant, participant type, normalized line, and affiliate identity; remove rows whose `closed_at` is present; and retain the returned `is_main_line` value. Update your cursor to the new `delta_last_id` from the response.

Market delta is scoped by sport, not date. Ignore a delta row whose `event_id` is absent from your latest snapshot, and use the next snapshot to refresh event membership and schedule metadata.

<Note>
  Polling futures too? The futures snapshot (`GET /api/v2/sports/{sportID}/futures`) provides the same `meta.delta_last_id` bootstrap cursor, but futures market IDs are **not** in the delta feed's default set — pass them explicitly (e.g. `market_ids=1141`). See the [Futures guide](/guides/futures).
</Note>

## Choosing Between Event and Market Delta

| Endpoint | Returns | Cursor type | Bootstrap source | Best for |
| - | - | - | - | - |
| `GET /api/v2/markets/delta` | Individual price changes only | Integer | Events snapshot `meta.delta_last_id` | Odds-focused apps (recommended for price polling) |
| `GET /api/v2/delta` | Full event objects (scores, status, markets) | Ordered UUID | The endpoint's own response `meta.delta_last_id` | Apps that need full event-object changes alongside odds |

The market delta is usually the cheapest option because it returns only the specific prices that changed, rather than the entire event object. The cursors are not interchangeable — the integer from the events snapshot bootstraps `markets/delta` only, and `/api/v2/delta` rejects it with a `400`.

## Recommended Polling Intervals

Match your polling frequency to your use case. Faster polling uses more data points and increases the chance of hitting your burst limit if you parallelize heavily.

| Use Case | Interval | Endpoint | Notes |
| - | - | - | - |
| Live odds screen | 5–10s | Market delta | Known zero-delay access only |
| Pre-match odds monitoring | 30–60s | Market delta or narrow snapshots | Use snapshots on delayed or unknown access |
| Live scores | 15–30s | Event delta | Score updates come in bursts during play |
| Pre-match schedules | 5 min | Full events | Schedules rarely change close to game time |
| Historical/closing lines | On demand | Openers/closing | Fetch once after the event starts or ends |

<Info>
  On a WebSocket-enabled tier, use the [WebSocket endpoint](/api-reference/v2/websocket) instead of polling when you need sub-second odds updates across every book or play and game-stat updates at play latency. For supported live games, live data trails the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay. WebSocket traffic does not increment the HTTP request counter, but pushed messages are still metered as data points.
</Info>

## Cache TTL Recommendations

Not all data changes at the same rate. Cache aggressively for reference data and use shorter TTLs for live data.

| Data Type | Recommended TTL | Endpoint |
| - | - | - |
| Sports list | 24 hours | `GET /api/v2/sports` |
| Affiliates list | 24 hours | `GET /api/v2/affiliates` |
| Teams | 6 hours | `GET /api/v2/sports/{id}/teams` |
| Market definitions | 6 hours | `GET /api/v2/markets` |
| Events (pre-match) | 5 minutes | `GET /api/v2/sports/{id}/events/{date}` |
| Events (live) | Use delta | `GET /api/v2/delta` or `GET /api/v2/markets/delta` |
| Market prices | Real-time | Delta endpoint or WebSocket |

## Staleness Guards

Your delta cursor can become stale if you stop polling for an extended period. When this happens, the delta endpoint may return an error or skip events that changed while you were away.

**How to detect stale data:**

* Track the `updated_at` timestamp on your cached events. If the newest update is more than 5 minutes old during a live game window, your data may be stale.
* An empty delta response can mean that no matching prices changed; it does not by itself indicate a stale cursor.
* A `400` cursor error can require one fresh snapshot bootstrap on known zero-delay access. Stop on authentication, entitlement, or rate-limit responses and handle them through your scheduler or account flow.

**How to recover:**

1. On one cursor error, fetch one fresh full snapshot from the events endpoint
2. On known zero-delay access, extract its valid `delta_last_id` and resume polling
3. If the next delta response still has a cursor error, stop and investigate; do not loop re-bootstrap requests

## Watch Your Usage Headers

Every production poller should log and monitor:

* `X-Datapoints`
* `X-Datapoints-Used`
* `X-Datapoints-Remaining`
* `X-Datapoints-Reset`
* `X-Rate-Limit`

That gives you the feedback loop to tune filters and polling intervals before users start hitting limits.

## Code Example: Python Polling Loop

This bounded example uses market delta only when a known entitlement establishes zero delay. Delayed or unknown access repeats narrow snapshots instead. It is intentionally finite so callers can apply their own scheduler, retry policy, and alerting.

```python theme={null}
import os
import requests
import time
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation

API_KEY = os.environ["THERUNDOWN_API_KEY"]
BASE = "https://therundown.io/api/v2"
SPORT_ID = 3  # MLB
SNAPSHOT_MARKET_IDS = "1,2,3,41,42,43"
DELTA_MARKET_IDS = "1,2,3,41,42,43"
CORE_MARKET_ID_SET = frozenset(DELTA_MARKET_IDS.split(","))
AFFILIATE_IDS = "19,23"
REQUEST_TIMEOUT = 10
POLL_INTERVAL = 5
SNAPSHOT_INTERVAL = 60
MAX_CYCLES = 12
MAX_DELTA_PAGES = 3
# Set only from a known entitlement. Do not infer it from a missing header.
KNOWN_ZERO_DELAY_ENTITLEMENT = False

events = {}
prices = {}

def positive_cursor(value):
    if isinstance(value, bool):
        return None
    if isinstance(value, int) and value > 0:
        return str(value)
    if isinstance(value, str) and value.isdigit() and int(value) > 0:
        return str(int(value))
    return None

def normalized_line(value):
    if value is None or value == "":
        value = 0  # A moneyline may omit its snapshot line value.
    if isinstance(value, bool):
        return None
    try:
        line = Decimal(str(value))
    except (InvalidOperation, ValueError):
        return None
    if not line.is_finite():
        return None
    if line == 0:
        return "0"  # Treat 0 and -0 as the same moneyline identity.
    return format(line.normalize(), "f")

def row_key(row):
    fields = ("event_id", "market_id", "participant_id", "participant_type", "affiliate_id")
    values = tuple(row.get(field) for field in fields)
    line = normalized_line(row.get("line"))
    if not all(value is not None for value in values) or line is None:
        return None
    event_id, market_id, participant_id, participant_type, affiliate_id = values
    return (str(event_id), str(market_id), str(participant_id), str(participant_type), line, str(affiliate_id))

def apply_price(row):
    if str(row.get("event_id")) not in events:
        return  # The delta stream can include another date's event.
    if str(row.get("market_id")) not in CORE_MARKET_ID_SET:
        return  # Keep this core-odds cache within its requested market scope.
    key = row_key(row)
    if key is None:
        return
    if row.get("closed_at"):
        prices.pop(key, None)
    else:
        # Replacing the complete row applies both price and is_main_line changes.
        prices[key] = row

def load_snapshot_prices(event):
    for market in event.get("markets", []):
        for participant in market.get("participants", []):
            for line in participant.get("lines", []):
                for affiliate_id, price in line.get("prices", {}).items():
                    apply_price({
                        **price,
                        "event_id": event.get("event_id"),
                        "market_id": market.get("market_id"),
                        "participant_id": participant.get("id"),
                        "participant_type": participant.get("type"),
                        "line": line.get("value", 0),
                        "affiliate_id": affiliate_id,
                    })

def snapshot_allows_delta(resp):
    raw_delay = resp.headers.get("X-Data-Delay-Seconds")
    if raw_delay is None:
        return False
    try:
        delay_seconds = int(raw_delay)
    except ValueError:
        return False
    return delay_seconds == 0 and KNOWN_ZERO_DELAY_ENTITLEMENT

def fetch_full_snapshot():
    """Load one narrow UTC-day snapshot and return its valid cursor, if any."""
    today_utc = datetime.now(timezone.utc).date().isoformat()
    resp = requests.get(
        f"{BASE}/sports/{SPORT_ID}/events/{today_utc}",
        headers={"X-TheRundown-Key": API_KEY},
        params={
            "market_ids": SNAPSHOT_MARKET_IDS,
            "affiliate_ids": AFFILIATE_IDS,
            "main_line": "true",
        },
        timeout=REQUEST_TIMEOUT,
    )
    if resp.status_code in (401, 403, 429):
        return None, False, resp.status_code
    resp.raise_for_status()
    data = resp.json()

    events.clear()
    prices.clear()
    for event in data.get("events", []):
        events[event["event_id"]] = event
        load_snapshot_prices(event)

    cursor = positive_cursor(data.get("meta", {}).get("delta_last_id"))
    print(f"Loaded {len(events)} events, cursor={cursor}")
    return cursor, snapshot_allows_delta(resp), 200

def poll_market_delta(last_id):
    """Apply up to MAX_DELTA_PAGES and return a cursor or a stop reason."""
    cursor = last_id
    for _ in range(MAX_DELTA_PAGES):
        resp = requests.get(
            f"{BASE}/markets/delta",
            headers={"X-TheRundown-Key": API_KEY},
            params={
                "last_id": cursor,
                "sport_id": SPORT_ID,
                "market_ids": DELTA_MARKET_IDS,
                "affiliate_ids": AFFILIATE_IDS,
            },
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code in (401, 403):
            return None, "authorization_or_entitlement"
        if resp.status_code == 429:
            return None, "rate_limited"
        if resp.status_code == 400:
            return None, "cursor_error"
        resp.raise_for_status()
        data = resp.json()
        for change in data.get("deltas", []):
            apply_price(change)
        next_cursor = positive_cursor(data.get("meta", {}).get("delta_last_id"))
        if not next_cursor:
            return None, "invalid_cursor"
        cursor = next_cursor
        if not data.get("meta", {}).get("has_more"):
            return cursor, None
    return None, "page_limit"

cursor, use_delta, status = fetch_full_snapshot()
if status in (401, 403, 429):
    raise RuntimeError(f"Snapshot stopped with HTTP {status}")

if not use_delta:
    for _ in range(MAX_CYCLES):
        time.sleep(SNAPSHOT_INTERVAL)
        _, _, status = fetch_full_snapshot()
        if status in (401, 403, 429):
            break
elif not cursor:
    raise RuntimeError("Zero-delay snapshot did not provide a valid cursor")
else:
    rebootstrap_used = False
    for _ in range(MAX_CYCLES):
        time.sleep(POLL_INTERVAL)
        cursor, reason = poll_market_delta(cursor)
        if reason in ("authorization_or_entitlement", "rate_limited", "page_limit"):
            break
        if reason and not rebootstrap_used:
            rebootstrap_used = True
            cursor, use_delta, status = fetch_full_snapshot()
            if not use_delta or not cursor or status != 200:
                break
        elif reason:
            break
```

## WebSocket vs. Polling Decision Guide

| Factor | REST Polling | WebSocket |
| - | - | - |
| Latency | 5–60s depending on interval | Real-time odds updates are sub-second across every book. Supported live game stats stream within seconds of corresponding play updates (\~15–20s behind on-field action) |
| Usage impact | Consumes data points and counts toward HTTP burst limit | Consumes data points but does not increment the HTTP request counter |
| Implementation complexity | Simple HTTP requests | Requires connection management, reconnection logic |
| Data freshness | As fresh as your poll interval | Real-time |
| Reliability | Each request is independent | Must handle disconnects and reconnections |
| Best for | Pre-match monitoring, low-frequency updates | Live odds screens, real-time dashboards |

<Note>
  Many production applications use **both**: WebSocket for live windows, with delta polling as a fallback when the socket disconnects or for lower-tier keys that do not have WebSocket access. See the [Building an Odds Screen](/guides/building-odds-screen) guide for this pattern in practice.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.