Skip to main content
The laptimes.py script fetches detailed lap-by-lap timing data for each driver in a race.

Script Overview

Location: ~/workspace/source/laptimes.py The script:
  1. Fetches lap times using pagination (API returns max 100 records per request)
  2. Combines all paginated data into a single file
  3. Respects rate limits during pagination
  4. Saves complete lap timing data
Lap time data is available from 1996 onwards. Earlier races do not have lap-by-lap timing information.

API Endpoint with Pagination

url = f"https://api.jolpi.ca/ergast/f1/{year}/{round_num}/laps.json?limit={LIMIT}&offset={offset}"
The API paginates lap times because races can have thousands of individual lap records (number of drivers × number of laps).

Key Configuration

BASE_URL = "https://api.jolpi.ca/ergast/f1"
LIMIT = 100  # Number of records per request
RATE_LIMIT_BURST = 4  # Max requests per second
RATE_LIMIT_SUSTAINED = 500  # Max requests per hour

Fetch Function with Pagination

def fetch_laptimes(year, round_num):
    """Fetch all lap times for a specific race with pagination."""
    all_data = None
    offset = 0
    total_records = None

    while total_records is None or offset < total_records:
        url = f"{BASE_URL}/{year}/{round_num}/laps.json?limit={LIMIT}&offset={offset}"
        logger.info(f"Fetching data from: {url}")

        try:
            response = requests.get(url)
            response.raise_for_status()
            data = response.json()

            # Initialize all_data with the first response
            if all_data is None:
                all_data = data
                total_records = int(data["MRData"]["total"])
                logger.info(f"Total records to fetch: {total_records}")
            else:
                # Append new lap data to existing data
                if "Laps" in data["MRData"]["RaceTable"]["Races"][0]:
                    all_data["MRData"]["RaceTable"]["Races"][0]["Laps"].extend(
                        data["MRData"]["RaceTable"]["Races"][0]["Laps"]
                    )

            offset += LIMIT

            # Respect rate limits
            time.sleep(1 / RATE_LIMIT_BURST)  # Ensure we don't exceed burst limit

        except requests.exceptions.RequestException as e:
            if hasattr(e.response, "status_code") and e.response.status_code == 429:
                logger.warning("Rate limit exceeded. Waiting for 60 seconds...")
                time.sleep(60)  # Wait longer if we hit rate limit
                continue
            logger.error(f"Error fetching data: {e}")
            return None

    return all_data
Races with many laps and many drivers can require 10+ paginated requests. The script automatically handles this but may take some time to complete.

Save Function

def save_laptimes(data, year, round_num):
    """Save lap times data to laptimes.json in the appropriate folder."""
    # Get race name from the data
    race_name = (
        data["MRData"]["RaceTable"]["Races"][0]["raceName"].lower().replace(" ", "-")
    )

    # Create directory structure
    year_path = Path(str(year))
    race_folder = year_path / race_name

    # Create directories if they don't exist
    race_folder.mkdir(parents=True, exist_ok=True)

    # Save the data
    output_file = race_folder / "laptimes.json"
    with open(output_file, "w") as f:
        json.dump(data, f, indent=2)
    logger.info(f"Saved lap times data to {output_file}")

Main Function

def main(year, round_num):
    """Main function to fetch lap times for a specific round in a specific year."""
    logger.info(f"Fetching lap times for year {year}, round {round_num}")

    # Fetch lap times data
    lap_data = fetch_laptimes(year, round_num)

    if lap_data:
        # Save the data
        save_laptimes(lap_data, year, round_num)
        logger.info(
            f"Successfully fetched and saved lap times for year {year}, round {round_num}"
        )
    else:
        logger.error(f"Failed to fetch lap times for year {year}, round {round_num}")

Output Structure

Stored at: {year}/{race-name}/laptimes.json
{
  "MRData": {
    "series": "f1",
    "url": "https://api.jolpi.ca/ergast/f1/1996/1/laps.json",
    "limit": "100",
    "offset": "0",
    "total": "812",
    "RaceTable": {
      "season": "1996",
      "round": "1",
      "Races": [
        {
          "season": "1996",
          "round": "1",
          "raceName": "Australian Grand Prix",
          "Circuit": {
            "circuitId": "albert_park",
            "circuitName": "Albert Park Grand Prix Circuit",
            "Location": {
              "lat": "-37.8497",
              "long": "144.968",
              "locality": "Melbourne",
              "country": "Australia"
            }
          },
          "date": "1996-03-10",
          "Laps": [
            {
              "number": "1",
              "Timings": [
                {
                  "driverId": "villeneuve",
                  "position": "1",
                  "time": "1:43.702"
                },
                {
                  "driverId": "damon_hill",
                  "position": "2",
                  "time": "1:44.243"
                }
              ]
            },
            {
              "number": "2",
              "Timings": [
                {
                  "driverId": "villeneuve",
                  "position": "1",
                  "time": "1:32.185"
                }
              ]
            }
          ]
        }
      ]
    }
  }
}

Data Structure

Each lap contains:
FieldDescription
numberLap number
TimingsArray of driver lap times for this lap
Each timing entry contains:
FieldDescription
driverIdDriver identifier
positionPosition on track at end of lap
timeLap time (mm:ss.sss format)

Usage Example

Single Race

from laptimes import main

main(2024, 1)  # Fetch lap times for 2024 Bahrain GP

Multiple Races

for round_num in range(1, 23):
    main(2024, round_num)
    logger.info(f"Completed lap times for round {round_num}")

Pagination Example

For a typical modern F1 race:
  • 20 drivers × 60 laps = 1,200 lap time records
  • At 100 records per request = 12 API requests
  • At 4 requests/second = ~3 seconds per race
Fetching data from: .../laps.json?limit=100&offset=0
Total records to fetch: 1200
Fetching data from: .../laps.json?limit=100&offset=100
Fetching data from: .../laps.json?limit=100&offset=200
...
Saved lap times data to 2024/bahrain-grand-prix/laptimes.json

Performance Tips

The script automatically sleeps between requests to respect rate limits. Fetching lap times for an entire season may take several minutes.

Optimizing for Multiple Races

import time
from laptimes import main

races = [(2024, 1), (2024, 2), (2024, 3)]

for year, round_num in races:
    start_time = time.time()
    main(year, round_num)
    elapsed = time.time() - start_time
    logger.info(f"Completed in {elapsed:.2f} seconds")

Data Availability

Lap time data is only available from 1996 onwards. Attempting to fetch lap times for earlier years will return empty results.

Logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[logging.FileHandler("laptimes_fetch.log"), logging.StreamHandler()]
)

Pitstops

Collect pit stop data next (2011+)

Race Results

Compare with race finishing positions

Build docs developers (and LLMs) love