Pagination and rate limiting
Most APIs cap how many results they return per request and how many requests you can make per minute — learn to navigate both limits reliably.
- Explain offset-based and cursor-based pagination
- Write a loop that fetches all pages until there are no more results
- Identify rate limit headers (X-RateLimit-Remaining, Retry-After)
- Design a polite request loop that backs off when limited
A public API rarely returns all of its data in one response. Ask for a hundred thousand records and the server will send you the first hundred and a way to ask for the next hundred. Similarly, if you fire requests as fast as Python can loop, the server will eventually tell you to slow down. Both limits are by design — they protect the API from being overwhelmed.
Offset-based pagination
The simpler model. The server accepts page and per_page (or limit and offset)
as query parameters. You increment the page number on each request until you get back
fewer items than you asked for — that signals the last page:
page = 1
per_page = 100
all_items = []
while True:
response = requests.get(
"https://api.example.com/items",
params={"page": page, "per_page": per_page},
)
response.raise_for_status()
items = response.json()
all_items.extend(items)
if len(items) < per_page:
break # last page — got fewer items than the page size
page += 1The termination condition — receiving fewer items than requested — is the idiomatic
signal. Some APIs include a total count in the response body; you can also use that
to calculate when you are done.
Cursor-based pagination
More common in modern APIs. Instead of a page number, the server returns an opaque
cursor (sometimes called next_cursor, after, or embedded in a next URL) that
encodes exactly where to start the next page. You pass it back on the next request:
cursor = None
all_items = []
while True:
params = {"per_page": 100}
if cursor:
params["after"] = cursor
response = requests.get("https://api.example.com/items", params=params)
response.raise_for_status()
body = response.json()
all_items.extend(body["items"])
cursor = body.get("next_cursor")
if not cursor:
break # no next cursor means no more pagesCursors are more robust than offset pagination for large, fast-changing datasets because they point at a position in the data rather than a page number that can shift as items are added or deleted.
Rate limiting
APIs limit how many requests you can make in a time window — typically expressed as
requests per minute or per hour. When you exceed the limit, the server responds with
429 Too Many Requests.
Two common response headers tell you what to do next:
X-RateLimit-Remaining— how many requests are left in the current window. Check this on every response; if it reaches zero, wait before the next request.Retry-After— the number of seconds to wait before retrying. Sent with 429 responses. The most direct signal: the server tells you exactly how long to wait.
Not every API sends these headers. If they are absent, use exponential backoff: wait 1 second after the first 429, 2 seconds after the second, 4 after the third, and so on. Cap the wait at something reasonable (60 seconds is common).
A polite request loop
Combining pagination with rate limit awareness:
import time
def fetch_all(base_url, params=None):
params = params or {}
params["per_page"] = 100
all_items = []
page = 1
while True:
params["page"] = page
response = requests.get(base_url, params=params)
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 5))
print(f"Rate limited — waiting {wait}s")
time.sleep(wait)
continue # retry the same page
response.raise_for_status()
items = response.json()
all_items.extend(items)
remaining = response.headers.get("X-RateLimit-Remaining")
if remaining and int(remaining) < 5:
time.sleep(1) # slow down proactively
if len(items) < 100:
break
page += 1
return all_itemsThe continue on a 429 retries the same page rather than advancing — important detail.
Without it, a rate-limited page would be silently skipped.
Where to go next
Next: lab — API report — combine everything from this module into a script that fetches all pages of a public API, aggregates the data, and writes a summary report.
Auth in practice
Pass API credentials safely by reading them from environment variables and attaching them as request headers — never from hardcoded strings.
Lab: API report
Fetch all todos from a public API, aggregate them by user, and write a plain-text summary report — end-to-end practice for the external integrations module.