# How Fresh Are Our Prices? Update Cadence and Timestamps
URL: https://sockodds.com/docs/info/freshness/

# How Fresh Are Our Prices? Update Cadence and Timestamps [#how-fresh-are-our-prices-update-cadence-and-timestamps]

SockOdds re-reads its source about every 2 minutes, stamps every bookmaker price with the time we last saw it (or, where the source gives a price no time of its own, the time of the record it arrived in), and marks a price we have not seen recently as unavailable rather than serving it as current. This page sets out exactly what the API does, and the fields you can check on every response so you never have to take our word for it.

Checked against the API's code on 6 October 2026. The thresholds the API is applying right now are published live at [https://api.sockodds.com/health](https://api.sockodds.com/health).

## The numbers [#the-numbers]

| What | Value | Where you can see it |
| --- | --- | --- |
| How often SockOdds pulls from its odds pipeline | About every 2 minutes: each pass is scheduled 120 seconds after the previous one | `info.lastUpdatedAt` moves forward; `freshness.newestSourceUpdatedAt` on `/health` |
| How long a price from the live price table counts as current | 10 minutes after we last saw it | `expiresAt` on the price; `freshness.quoteExpirySeconds` on `/health` |
| When a price or an event is treated as stale | 45 minutes without an update | `available` and `info.stale`; `freshness.staleAfterSeconds` on `/health` |
| Shared read cache in front of the database | At most 30 seconds, and cleared on every write from the pipeline | Availability is worked out again on every request, after the cache |
| Native WebSocket stream (Pro and Platform) | Re-reads the API every 30 seconds | A `snapshot` when anything changed, a `heartbeat` when nothing did |

## What "~2 min update frequency" covers [#what-2-min-update-frequency-covers]

The plan cards on [pricing](https://sockodds.com/pricing/) say **~2 min update frequency**. That is how often SockOdds pulls from its odds pipeline: each pass is scheduled 120 seconds after the previous one, and a pass itself takes some seconds, so passes land a little over two minutes apart. Developer, Rookie, Pro and Platform keys all read the same store, so they see the same freshness; plans differ in leagues, bookmakers and volume, not in how current the prices are.

It is **not** how often each bookmaker is checked. That happens upstream of us, and the cadence varies by bookmaker and league. So a price can be older than two minutes when it reaches you, and `lastUpdatedAt` tells you by how much. This is a pre-match and in-play reference feed refreshed every couple of minutes, not a low latency trading feed.

## The timestamps on every response [#the-timestamps-on-every-response]

| Field | Where | What it means |
| --- | --- | --- |
| `info.lastUpdatedAt` | Each event | When our odds pipeline last wrote this event's record. |
| `info.stale` | Each event | `true` when `info.lastUpdatedAt` is more than 45 minutes old. The consensus fields `bookOdds`, `bookSpread`, `bookOverUnder`, `fairOdds`, `fairSpread` and `fairOverUnder` are then `null` and `fairOddsAvailable` is `false`. |
| `lastUpdatedAt` | `odds..byBookmaker.` | When we last observed this bookmaker's price. |
| `expiresAt` | Same, on prices from our live price table | `lastUpdatedAt` plus 10 minutes. After it the price is served `available: false`. |
| `snapshotUpdatedAt` | Same, only when a price has no time of its own | The time of the event record the price arrived in. It is not a separate observation of the price. |
| `available` | Same | `false` when the price has expired, is more than 45 minutes old, or is suspended. Decided at the moment of your request. |
| `suspended` | Same, when present | `true` when the bookmaker shows the price but is not taking bets on it. `available` is then `false`. |
| `bookOddsAvailable` | Each odd | `true` when at least one bookmaker price on it is available. |
| `metadataUpdatedAt` | `/players/` and `/stats/` responses | When the reference catalogue behind those responses was generated. |

Two prices as they arrive, one live and one suspended:

```json
{
  "sportsbet": {
    "bookmakerID": "sportsbet",
    "odds": "-125",
    "decimal": 1.8,
    "available": true,
    "lastUpdatedAt": "2026-10-06T05:01:12.000Z",
    "expiresAt": "2026-10-06T05:11:12.000Z"
  },
  "tab": {
    "bookmakerID": "tab",
    "odds": "-118",
    "decimal": 1.85,
    "available": false,
    "suspended": true,
    "lastUpdatedAt": "2026-10-06T05:00:47.000Z",
    "expiresAt": "2026-10-06T05:10:47.000Z"
  }
}
```

## When a bookmaker goes stale [#when-a-bookmaker-goes-stale]

- Availability is decided on every request, from the clock at that moment. A price does not stay `available: true` because it was cached.
- A price from the live price table is served `available: false` once `expiresAt` passes, 10 minutes after we last saw it. Any price is served `available: false` once it is more than 45 minutes old.
- A suspended selection is listed with `available: false` and `suspended: true`, not hidden.
- A stale price stays listed under `byBookmaker`, so you can see the last price and when it was seen. When the event's record is next rewritten, a price is carried forward only if the source still lists it, so a bookmaker that has gone quiet drops out of `byBookmaker`.
- Events are never deleted. An event whose record has not been written for 45 minutes stays in the feed with `info.stale: true`.
- `oddsAvailable=true` on `/events/` returns only events with at least one available price.

## Check freshness yourself [#check-freshness-yourself]

Skip stale events and unavailable prices, then apply your own age limit to `lastUpdatedAt`:

```javascript
const res = await fetch("https://api.sockodds.com/v2/events/?leagueID=AFL&oddsAvailable=true", {
  headers: { "x-api-key": process.env.SOCKODDS_API_KEY },
});
const { data } = await res.json();
const now = Date.now();
for (const event of data) {
  if (event.info.stale) continue; // the event's record is over 45 minutes old
  for (const odd of Object.values(event.odds)) {
    for (const [bookmakerID, quote] of Object.entries(odd.byBookmaker)) {
      if (!quote.available) continue; // expired, stale or suspended
      const seen = Date.parse(quote.lastUpdatedAt ?? quote.snapshotUpdatedAt);
      const ageSeconds = Math.round((now - seen) / 1000);
      if (ageSeconds > 300) continue; // your own threshold: here, five minutes
      console.log(event.eventID, odd.oddID, bookmakerID, quote.odds, ageSeconds + "s old");
    }
  }
}
```

The thresholds the API is applying are on its health endpoint, which needs no key:

```bash
curl https://api.sockodds.com/health
```

```json
{
  "status": "ok",
  "pipeline": "healthy",
  "freshness": {
    "newestSourceUpdatedAt": "2026-10-06T05:02:41.000Z",
    "staleAfterSeconds": 2700,
    "quoteExpirySeconds": 600
  }
}
```

> `pipeline` is `healthy` while the newest event record is under 45 minutes old, `stale` after that, and `empty` before the first pull. Before you act on any price, check it on the bookmaker through `links.bookmakers`: prices move between our observations.

## Related [#related]

- [Handling odds](https://sockodds.com/docs/guides/handling-odds/)
- [Realtime data stream](https://sockodds.com/docs/guides/realtime-streaming-api/)
- [Best practices](https://sockodds.com/docs/info/best-practices/)
- [Compare SockOdds with other odds APIs](https://sockodds.com/compare/)

> Need help?[FAQ](https://sockodds.com/docs/faq/) · [Email](mailto:api@sockodds.com) · [Contact](https://sockodds.com/contact-us/)
