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.
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.
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.
Free-tier limits by minute, hour and day
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
- 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.
Internal links
- How to get weather data for many locations in Python β batching in practice
- How to get historical weather data in Python β the archive API
- OSMnx download fails or times out β the same politeness with Overpass
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.