tutorial

How to Cache Odds API Responses Safely for Pre-Match Football Data

Fetching pre-match football odds from an API repeatedly can quickly exhaust your rate limits and slow down your application. The solution is to cache odds API responses safely. This tutorial explains how to implement effective caching strategies to reduce API calls, improve performance, and ensure you always have fresh, reliable data for your projects.

Caching is essential for any application consuming external data, especially when dealing with frequently updated information like sports odds. By storing responses locally for a set period, you avoid redundant API requests, which keeps you within your plan's limits and makes your application faster. For anyone building with a UK bookmaker odds API, understanding how to cache effectively is not just a best practice—it's a necessity to scale without breaking the bank or hitting 429 Too Many Requests errors. This approach helps you get odds API without scraping, providing a stable and efficient data flow.

abstract data flow diagram with cache layer, representing efficiency and speed

Prerequisites

Before we dive into the code, ensure you have the following set up:

  • UK Odds API Key: You'll need an API key from ukoddsapi.com. Sign up for a free account to get started.
  • Python 3.x: Our examples use Python.
  • requests library: For making HTTP requests. Install with pip install requests.
  • Redis server (optional, for persistence): If you want to implement persistent caching, you'll need a running Redis instance.
  • redis-py library (optional): For interacting with Redis from Python. Install with pip install redis.

Step 1: Understand Odds Volatility and API Limits

Pre-match football odds are dynamic but not volatile in the same way as in-play odds. They change based on team news, betting volume, and market shifts, but usually not every second. This characteristic makes them ideal candidates for caching. However, you still need to refresh them periodically to ensure accuracy.

Every API has rate limits. UK Odds API, for example, offers a Free tier with 300 requests/month and a Starter tier with 1,000 requests/hour. Hitting these limits means your application stops receiving data until the next reset period. Caching is your primary defence against this.

First, let's see how to fetch odds for a specific event without any caching. This will be our baseline.

import os
import requests
from datetime import datetime

# Replace with your actual API key or set as environment variable
API_KEY = os.environ.get("UKODDSAPI_KEY", "YOUR_API_KEY")
BASE_URL = "https://api.ukoddsapi.com"
HEADERS = {"X-Api-Key": API_KEY}

def fetch_odds_from_api(event_id: str):
    """Fetches full odds for a specific event from UK Odds API."""
    print(f"Fetching odds for event {event_id} from API...")
    try:
        response = requests.get(
            f"{BASE_URL}/v1/football/events/{event_id}/odds",
            headers=HEADERS,
            params={"package": "core", "odds_format": "decimal"},
            timeout=10,
        )
        response.raise_for_status()  # Raise an exception for HTTP errors
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"API request failed: {e}")
        return None

# Example usage: Find an event ID first
def get_an_event_id(date_str: str = "2026-04-29"):
    """Fetches a list of events and returns the first event_id with odds."""
    print(f"Fetching events for {date_str}...")
    try:
        response = requests.get(
            f"{BASE_URL}/v1/football/events",
            headers=HEADERS,
            params={"schedule_date": date_str, "has_odds": "true", "per_page": "1"},
            timeout=10,
        )
        response.raise_for_status()
        events_data = response.json()
        if events_data and events_data.get("events"):
            return events_data["events"][0]["event_id"]
        return None
    except requests.exceptions.RequestException as e:
        print(f"Error fetching events: {e}")
        return None

if __name__ == "__main__":
    event_id_to_fetch = get_an_event_id()
    if event_id_to_fetch:
        odds_data = fetch_odds_from_api(event_id_to_fetch)
        if odds_data:
            print(f"Successfully fetched odds for: {odds_data.get('event_title')}")
            # print(odds_data) # Uncomment to see full JSON
            first_market = odds_data["markets"][0]
            print(f"First market: {first_market['market_name']}")
            print(f"Kickoff: {odds_data['kickoff_utc']}")
        else:
            print("Failed to get odds data.")
    else:
        print("No event found with odds for the specified date.")

This Python snippet first retrieves an event_id for a scheduled fixture, then fetches its full pre-match odds. The odds_data JSON response contains fields like event_title, kickoff_utc, and an array of markets. Each market includes market_name and selections with odds and bookmaker_code. This is the data we want to cache.

{
  "schema_version": "1.0",
  "event_id": "EVT123456789",
  "event_title": "Manchester United vs Liverpool",
  "kickoff_utc": "2026-04-29T19:00:00Z",
  "summary": {
    "home_team": "Manchester United",
    "away_team": "Liverpool",
    "league_name": "Premier League"
  },
  "markets": [
    {
      "market_id": "MKT001",
      "market_name": "Match Winner",
      "market_group": "main",
      "selection_count": 3,
      "selections": [
        {
          "selection_name": "Manchester United",
          "line": null,
          "odds": 2.50,
          "bookmaker_code": "UO001",
          "status": "active"
        },
        {
          "selection_name": "Draw",
          "line": null,
          "odds": 3.40,
          "bookmaker_code": "UO001",
          "status": "active"
        },
        {
          "selection_name": "Liverpool",
          "line": null,
          "odds": 2.80,
          "bookmaker_code": "UO001",
          "status": "active"
        }
      ]
    }
  ],
  "note": "Example only — response is truncated."
}

This JSON shows a typical response for pre-match football odds. Notice the kickoff_utc timestamp. This is crucial for determining when data becomes stale or irrelevant.

Step 2: Implement a Simple In-Memory Cache

For smaller applications or development environments, a simple in-memory cache (like a Python dictionary) can be sufficient. This method stores data directly in your application's memory. It's fast but non-persistent, meaning the cache clears if your application restarts.

When you cache odds API responses safely explained, the key is to store not just the data, but also a timestamp of when it was fetched. This allows you to implement a Time-To-Live (TTL) or expiration policy. For pre-match odds, a cache duration of 5 to 15 minutes is often a good starting point, depending on how fresh your data needs to be.

import os
import requests
import time
from datetime import datetime, timedelta

# ... (API_KEY, BASE_URL, HEADERS, fetch_odds_from_api, get_an_event_id functions from Step 1) ...

# Simple in-memory cache
cache = {}
CACHE_DURATION_SECONDS = 60 * 10  # 10 minutes for pre-match odds

def get_odds_with_in_memory_cache(event_id: str):
    """
    Fetches odds, using an in-memory cache.
    If data is in cache and not expired, returns cached data.
    Otherwise, fetches from API and updates cache.
    """
    current_time = datetime.now()

    # Check if data is in cache and not expired
    if event_id in cache:
        cached_item = cache[event_id]
        if current_time - cached_item["timestamp"] < timedelta(seconds=CACHE_DURATION_SECONDS):
            print(f"Returning cached odds for event {event_id}.")
            return cached_item["data"]
        else:
            print(f"Cached odds for event {event_id} expired. Refreshing.")
            del cache[event_id] # Remove expired item

    # If not in cache or expired, fetch from API
    odds_data = fetch_odds_from_api(event_id)
    if odds_data:
        cache[event_id] = {
            "data": odds_data,
            "timestamp": current_time,
            "kickoff_utc": datetime.fromisoformat(odds_data["kickoff_utc"].replace('Z', '+00:00'))
        }
    return odds_data

if __name__ == "__main__":
    event_id_to_fetch = get_an_event_id()
    if event_id_to_fetch:
        print("\n--- First call (should fetch from API) ---")
        odds_1 = get_odds_with_in_memory_cache(event_id_to_fetch)
        if odds_1:
            print(f"Fetched: {odds_1.get('event_title')}")

        print("\n--- Second call (should use cache) ---")
        odds_2 = get_odds_with_in_memory_cache(event_id_to_fetch)
        if odds_2:
            print(f"Fetched: {odds_2.get('event_title')}")

        print(f"\nWaiting {CACHE_DURATION_SECONDS + 5} seconds to expire cache...")
        time.sleep(CACHE_DURATION_SECONDS + 5)

        print("\n--- Third call (should fetch from API after expiry) ---")
        odds_3 = get_odds_with_in_memory_cache(event_id_to_fetch)
        if odds_3:
            print(f"Fetched: {odds_3.get('event_title')}")
    else:
        print("No event found with odds for the specified date.")

This get_odds_with_in_memory_cache function demonstrates a basic cache-aside pattern. It checks the cache first. If the data is present and fresh, it returns it immediately. Otherwise, it calls the API, stores the new data with a timestamp, and then returns it. This simple method helps to how to cache odds API responses safely integration into your application logic.

conceptual image of data flowing into a cache, with a clock icon indicating expiration

Step 3: Add Persistence with Redis

While an in-memory cache is simple, it's not suitable for production applications that need to scale or persist data across restarts. For robust caching, a dedicated key-value store like Redis is ideal. Redis offers excellent performance and built-in features like Time-To-Live (TTL) for keys, making cache management much easier.

Using Redis for your pre-match football odds JSON caching provides persistence, allowing multiple application instances to share the same cache. This is crucial for distributed systems or when deploying to cloud environments.

import os
import requests
import time
import json
from datetime import datetime, timedelta
import redis

# ... (API_KEY, BASE_URL, HEADERS, fetch_odds_from_api, get_an_event_id functions from Step 1) ...

# Redis connection
try:
    # Assumes Redis is running on localhost:6379
    # For production, use environment variables for host, port, password
    redis_client = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True)
    redis_client.ping()
    print("Successfully connected to Redis.")
except redis.exceptions.ConnectionError as e:
    print(f"Could not connect to Redis: {e}. Falling back to no caching or in-memory if available.")
    redis_client = None # Set to None if connection fails

CACHE_DURATION_SECONDS_REDIS = 60 * 15  # 15 minutes for pre-match odds in Redis

def get_odds_with_redis_cache(event_id: str):
    """
    Fetches odds, using Redis as a cache.
    If data is in cache and not expired, returns cached data.
    Otherwise, fetches from API and updates cache.
    """
    if not redis_client:
        print("Redis client not available. Fetching directly from API.")
        return fetch_odds_from_api(event_id)

    cache_key = f"odds:{event_id}"
    cached_data_json = redis_client.get(cache_key)

    if cached_data_json:
        print(f"Returning cached odds for event {event_id} from Redis.")
        return json.loads(cached_data_json)
    else:
        print(f"Odds for event {event_id} not in Redis cache or expired. Fetching from API.")
        odds_data = fetch_odds_from_api(event_id)
        if odds_data:
            # Store in Redis with a TTL
            redis_client.setex(cache_key, CACHE_DURATION_SECONDS_REDIS, json.dumps(odds_data))
        return odds_data

if __name__ == "__main__":
    event_id_to_fetch = get_an_event_id()
    if event_id_to_fetch:
        print("\n--- First call with Redis (should fetch from API) ---")
        odds_1 = get_odds_with_redis_cache(event_id_to_fetch)
        if odds_1:
            print(f"Fetched: {odds_1.get('event_title')}")

        print("\n--- Second call with Redis (should use cache) ---")
        odds_2 = get_odds_with_redis_cache(event_id_to_fetch)
        if odds_2:
            print(f"Fetched: {odds_2.get('event_title')}")

        print(f"\nWaiting {CACHE_DURATION_SECONDS_REDIS + 5} seconds to expire Redis cache...")
        time.sleep(CACHE_DURATION_SECONDS_REDIS + 5)

        print("\n--- Third call with Redis (should fetch from API after expiry) ---")
        odds_3 = get_odds_with_redis_cache(event_id_to_fetch)
        if odds_3:
            print(f"Fetched: {odds_3.get('event_title')}")
    else:
        print("No event found with odds for the specified date.")

This code uses redis-py to connect to a Redis server. The get_odds_with_redis_cache function attempts to retrieve data from Redis first. If it's not found (cache miss) or if the key has expired (Redis handles TTL automatically with setex), it fetches from the UK Odds API and stores the JSON response in Redis. This is a robust way to how to cache odds API responses safely in a production environment.

Common Mistakes When Caching Odds Data

Even with a solid caching strategy, it's easy to make mistakes that undermine its effectiveness or lead to stale data.

  • Caching for too long: If your CACHE_DURATION_SECONDS is too high, your application will display outdated odds. Pre-match odds can shift, especially closer to kickoff.
  • Caching for too short: A very short cache duration (e.g., 30 seconds) might not provide enough benefit to justify the overhead, as you'll still be hitting the API frequently.
  • Not handling cache misses: Always have a fallback to fetch from the API if the cache doesn't contain the requested data.
  • Ignoring kickoff_utc for cache expiry: For pre-match odds, the data becomes irrelevant once the match starts. Your cache invalidation logic should ideally consider the kickoff_utc timestamp and automatically expire data for events that have already begun.
  • Inconsistent cache keys: Ensure your cache keys are unique and consistently generated for each event_id and any other relevant parameters (e.g., package, odds_format).
  • Not monitoring cache hit ratio: Track how often your application successfully retrieves data from the cache versus hitting the API. A low hit ratio indicates an inefficient caching strategy.
  • Over-reliance on client-side caching: While browser caching is useful for static assets, it's not reliable for dynamic data like odds. Server-side caching is crucial for data integrity and API rate limit management.

Caching Strategies Comparison

Choosing the right caching strategy depends on your application's scale, data freshness requirements, and budget. Here's a comparison of common approaches for managing UK bookmaker odds API data:

Strategy Complexity Performance Scalability Data Freshness Cost
No Caching Low Low Low Real-time High (API calls)
In-Memory Cache Low High Low Configurable Low (RAM usage)
Redis Cache Medium High High Configurable Medium (Redis hosting/management)
CDN Caching High Very High Very High Configurable Medium-High (CDN service fees)

No caching is the simplest but least efficient, quickly leading to rate limit issues. In-memory caching is great for single-instance applications but lacks persistence and scalability. Redis provides a robust, scalable, and persistent solution, ideal for most production environments. CDN caching is typically used for static content or highly distributed APIs, but it can be adapted for dynamic content with careful configuration, though it's often overkill for direct API consumption unless you're serving a massive global audience. For most developers using an odds API without scraping, a Redis-backed cache offers the best balance of performance, scalability, and cost.

FAQ

What is the ideal cache duration for pre-match football odds?

For pre-match football odds, a cache duration of 5 to 15 minutes is generally a good starting point. This provides a balance between data freshness and reducing API calls. Adjust this based on how sensitive your application is to minor odds fluctuations.

How does caching help with API rate limits?

Caching significantly reduces the number of direct API requests your application makes. Instead of fetching the same data repeatedly, your app retrieves it from the local cache. This keeps your API call count low, helping you stay within your plan's rate limits.

Can I cache data for multiple events simultaneously?

Yes, you can cache data for multiple events. Each event's odds data should be stored under a unique cache key (e.g., odds:EVENT_ID). This allows you to retrieve specific event odds from the cache without affecting others.

What happens if a cached event is cancelled?

If an event is cancelled, the cached data will become stale. Your caching strategy should include a mechanism to invalidate or refresh data for cancelled events. You might need to poll an events endpoint periodically to check for status changes and then clear relevant cache entries.

Is a simple in-memory cache sufficient for small projects?

For very small projects, local development, or single-process applications, a simple in-memory cache can be sufficient. However, for anything requiring persistence across restarts, shared access among multiple processes, or scalability, a dedicated caching solution like Redis is recommended.

Caching is a critical component for any application consuming external APIs, especially for dynamic data like pre-match football odds. By implementing a smart caching strategy, you can drastically improve your application's performance, reduce operational costs, and stay well within API rate limits. Whether you opt for a simple in-memory solution or a robust Redis-backed cache, the principles remain the same: store data intelligently, manage its lifecycle, and always prioritize data freshness.

To start building with reliable pre-match football odds, explore the UK Odds API.