Fixing Open-Meteo "429 Too Many Requests" Errors in Python

On this page

Problem statement

A script that loops over dates or locations calling the Open-Meteo API runs fine for a while, then fails with HTTP 429 and a JSON body saying the minutely, hourly or daily request limit was exceeded. The free API is generous but not unlimited. It counts requests per minute, hour and day, and it weighs a request by its size: each location in a multi-location request counts, and long periods or many variables count as more than one call. Retrying immediately makes it worse.

This page builds a small, polite Open-Meteo client in Python with four defences: a disk cache so no request is ever sent twice, batching of locations, a pause between requests, and a retry with back-off when a 429 does come. It measures how many network requests a typical job needs with and without the cache.

Quick answer

Cache every response by its parameters, and back off on 429 instead of retrying at once:

def get(url, params, cache=Path("data/cache"), pause=2.0, tries=6):
    cache.mkdir(exist_ok=True)
    f = cache / (hashlib.md5((url + json.dumps(params, sort_keys=True)).encode()).hexdigest() + ".json")
    if f.exists():
        STATS["cache"] += 1
        return json.loads(f.read_text())
    for attempt in range(tries):
        STATS["network"] += 1
        r = SESSION.get(url, params=params, timeout=300)
        if r.status_code == 429:
            STATS["429"] += 1
            time.sleep(60 * (attempt + 1))                     # the limits are per minute, hour and day
            continue
        r.raise_for_status()
        f.write_text(r.text); time.sleep(pause)
        return r.json()
    raise RuntimeError(f"still rate-limited after {tries} tries: {r.text[:200]}")

shutil.rmtree("data/cache", ignore_errors=True)                   # start from an empty cache, as on a first run
job = [{"latitude": 38.8472, "longitude": -77.0345, "start_date": f"{y}-01-01", "end_date": f"{y}-12-31", "daily": "temperature_2m_max"} for y in range(2015, 2025)]
for p in job:
    get(ARCHIVE, p)
first = dict(STATS)
for p in job:
    get(ARCHIVE, p)
print(f"first pass: {first['network']} network requests, {first['429']} rate-limited; second pass: {STATS['network'] - first['network']} network requests, {STATS['cache'] - first['cache']} from the cache")
first pass: 10 network requests, 0 rate-limited; second pass: 0 network requests, 10 from the cache

The first pass sent 10 network requests, none rate-limited thanks to the pause. The second pass, the same job re-run, sent none: all 10 came from the cache.

A polite request loop for the Open-Meteo API. a polite clientcache keyall parameterscached?return itrequestSession429?wait, retrysave, pause2 sSecond run: 0 network requests spatialworkflow.io
A polite client.

What a 429 says, fewer larger requests, batched locations

1. What a 429 looks like

The error body explains which limit was hit. Parse it rather than just the status code:

example = {"error": True, "reason": "Minutely API request limit exceeded. Please try again in one minute."}
reason = example["reason"]
window = "minute" if "Minutely" in reason else "hour" if "Hourly" in reason else "day"
print(f"limit hit: per {window}; wait at least one {window} before retrying")
limit hit: per minute; wait at least one minute before retrying

The reason names the window, here the minute, so the client knows how long to wait.

2. Fewer, larger requests

Ten one-year requests can be one ten-year request. Compare the number of HTTP requests:

one = get(ARCHIVE, {"latitude": 38.8472, "longitude": -77.0345, "start_date": "2015-01-01", "end_date": "2024-12-31", "daily": "temperature_2m_max"})
print(f"one request returned {len(one['daily']['time']):,} days, the same as the {len(job)} yearly requests together")
one request returned 3,653 days, the same as the 10 yearly requests together

One request returned all 3,653 days of 2015-2024, the same data as the ten yearly requests.

Network requests for the same job on a first and a second run. requests sentfirst run10 requestssecond run0 (all cached)one 10-year request1 request, same 3,653 days0510Ten yearly requests vs one decade request spatialworkflow.io
Requests sent.

3. Batch locations

Several locations go in one request as comma-separated coordinates. It saves round trips; each location still counts toward the limit:

pts = [(38.8472, -77.0345), (39.29, -76.61), (38.90, -77.45)]
b = get(ARCHIVE, {"latitude": ",".join(str(a) for a, _ in pts), "longitude": ",".join(str(o) for _, o in pts),
                  "start_date": "2024-07-01", "end_date": "2024-07-31", "daily": "temperature_2m_max"})
print(f"{len(b)} locations in one response; July 2024 mean maxima {[round(sum(x['daily']['temperature_2m_max']) / 31, 1) for x in b]} Β°C")
3 locations in one response; July 2024 mean maxima [31.8, 31.6, 32.3] Β°C

One response holds all three locations in order: July 2024 mean maxima of 31.8 Β°C for Washington, 31.6 Β°C for Baltimore and 32.3 Β°C for Dulles.

A cache you can trust

The data used on this page

The Open-Meteo historical weather API, free for non-commercial use within its published limits (a few hundred calls per minute and about 10,000 per day), queried for Washington, Baltimore and Dulles with a requests.Session.

import hashlib
import json
import shutil
import time
from pathlib import Path

import requests

Path("data").mkdir(exist_ok=True)
ARCHIVE = "https://archive-api.open-meteo.com/v1/archive"
SESSION = requests.Session()
SESSION.headers["User-Agent"] = "spatialworkflow-guide/1.0"
STATS = {"network": 0, "cache": 0, "429": 0}
print("client ready")
client ready

The client is ready.

Example 1: a cache you can trust

A cache must key on every parameter, including dates and variables, or it returns the wrong data. Check that two requests differing only in a variable get different cache files:

k = lambda p: hashlib.md5((ARCHIVE + json.dumps(p, sort_keys=True)).encode()).hexdigest()
a = dict(job[0]); bb = dict(job[0], daily="temperature_2m_min")
print(f"different cache keys for different variables: {k(a) != k(bb)}; same key regardless of parameter order: {k(dict(reversed(list(a.items())))) == k(a)}")
different cache keys for different variables: True; same key regardless of parameter order: True

The cache key changes with any parameter and ignores their order, so the cache can never return the wrong data.

The Open-Meteo rate-limit windows and what to do. which limit, how longwaitprevent withminutelya minutepause between callshourlyan hourfewer, larger requestsdailyuntil tomorrowcache, key, or self-hostThe 429 reason names the window spatialworkflow.io
Which limit, how long.

Free-tier limits by minute, hour and day

Open-Meteo 429 errors: causes and fixes. 429 causesCauseFixtight loop of requestspause and back offsame request re-rundisk cachemany locations at onceeach counts; batch smallshared IP addressothers count tooFree tier: non-commercial use spatialworkflow.io
429 causes.

Open-Meteo's free tier is meant for non-commercial and moderate use, and it enforces that with limits per minute, per hour and per day, counted per client IP address. A request is weighted by its cost: every location in a multi-location request counts, and requests with many variables or long periods count as several. When a limit is exceeded, the API answers with status 429 and a reason naming the minutely, hourly or daily limit. Retrying in a tight loop only extends the block, and on a shared machine it can lock out everyone behind the same address.

The robust pattern combines four habits. A disk cache keyed on the full set of parameters means a script can be re-run, fixed and re-run again without repeating any request, and since historical data does not change, the cache never goes stale. Fewer, larger requests cut overhead. A short pause between requests keeps a batch job under the per-minute limit. A back-off on 429 waits out the window that was exceeded. For production or commercial use, Open-Meteo offers keyed plans with higher limits, and the open-source server can be self-hosted.

Forecasts, shared IPs and daily limits

Checklist for a robust Open-Meteo client. robust clientcache keyed on every parameterorder-independentSessionreuse connectionspausebetween requestsback off on 429minutes, not secondsstop on daily limitresume tomorrowHistorical data never goes stale spatialworkflow.io
Robust client.
  • Forecast data: do not cache forecasts indefinitely; key the cache on the fetch date as well.
  • Shared IPs: on cloud runners and university networks, other users count against the same limit.
  • openmeteo-requests: the official client adds caching and retries and uses a compact binary format.
  • Daily limit: a 429 for the daily limit will not clear in minutes; stop and resume the next day.

FAQ

How do I fix Open-Meteo 429 Too Many Requests?

Wait for the window named in the error to pass, then cache responses on disk, pause between requests, and retry with back-off.

What are Open-Meteo's rate limits?

The free tier allows a few hundred calls per minute and about 10,000 per day, with large requests counting as several calls; see the terms page for current numbers.

Does requesting many locations at once avoid the limit?

No; it saves round trips, but each location counts.

Should I cache Open-Meteo responses?

Yes for historical data, which does not change; key the cache on every parameter.