> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/TracingInsights-Archive/Stats/llms.txt
> Use this file to discover all available pages before exploring further.

# Lap Times

> Collecting lap-by-lap timing data using laptimes.py

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

<Note>
  Lap time data is available from 1996 onwards. Earlier races do not have lap-by-lap timing information.
</Note>

## API Endpoint with Pagination

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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
```

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

## Save Function

```python theme={null}
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

```python theme={null}
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`

```json theme={null}
{
  "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:

| Field     | Description                            |
| --------- | -------------------------------------- |
| `number`  | Lap number                             |
| `Timings` | Array of driver lap times for this lap |

Each timing entry contains:

| Field      | Description                     |
| ---------- | ------------------------------- |
| `driverId` | Driver identifier               |
| `position` | Position on track at end of lap |
| `time`     | Lap time (mm:ss.sss format)     |

## Usage Example

### Single Race

```python theme={null}
from laptimes import main

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

### Multiple Races

```python theme={null}
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

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

### Optimizing for Multiple Races

```python theme={null}
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

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

## Logging

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

## Related Scripts

<CardGroup cols={2}>
  <Card title="Pitstops" icon="wrench" href="/data-collection/pitstops">
    Collect pit stop data next (2011+)
  </Card>

  <Card title="Race Results" icon="flag-checkered" href="/data-collection/race-results">
    Compare with race finishing positions
  </Card>
</CardGroup>
