Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

The Airtable API: Rate Limits, Pagination, and the 100,000 Record Wall

Sean

Platform Writer

Aug 14, 2026
8 min read

The Airtable Web API is a clean REST interface with one number that shapes everything you build on it: five requests per second per base. That limit, plus cursor pagination at 100 records a page, means reading a large table takes longer than people expect, and it is the constraint to design around from the start rather than discover under load.

The Airtable API: Rate Limits, Pagination, and the 100,000 Record Wall

Airtable generates personalised API documentation for each of your bases, showing your actual tables and fields, which is genuinely useful and also means the general behaviour is less discussed. These are the parts that apply everywhere.

Table of contents

Authentication, and the token change

Airtable retired the old user API keys in favour of personal access tokens and OAuth. Tokens are scoped to specific bases and specific permissions, which is a considerable improvement over a single key granting access to everything in the account.

curl "https://api.airtable.com/v0/appXXXXXXXXXXXXXX/Tasks" \
  -H "Authorization: Bearer ${AIRTABLE_TOKEN}"

Create tokens with the narrowest scopes that work. A token for a reporting job needs data.records:read and nothing else, and granting schema or write access because it was easier is how a read-only integration ends up able to delete a table.

The base identifier begins with app and is visible in the URL when you have the base open, or in the API documentation Airtable generates for it. Table names work in the path, and so do table identifiers beginning with tbl.

Prefer the identifier. Table names are editable by anyone with access to the base, and an integration keyed on the name breaks silently the moment someone renames Tasks to Task List. The identifier does not change.

The rate limit that shapes everything

Five requests per second per base. Exceeding it returns a 429, and Airtable then requires a 30-second wait before accepting further requests from that token against that base.

That penalty is what makes this limit unforgiving. Most APIs let you retry after a short pause. Here, going over costs half a minute, so an integration that bursts and backs off performs dramatically worse than one that paces itself correctly from the start.

import time, requests
from collections import deque

class Limiter:
    """Stay under 5 requests per second across the whole process."""
    def __init__(self, rate=4, per=1.0):     # 4, not 5, for headroom
        self.rate, self.per = rate, per
        self.calls = deque()

    def wait(self):
        now = time.monotonic()
        while self.calls and now - self.calls[0] > self.per:
            self.calls.popleft()
        if len(self.calls) >= self.rate:
            time.sleep(self.per - (now - self.calls[0]) + 0.01)
        self.calls.append(time.monotonic())

limiter = Limiter()

def get(url, **kwargs):
    limiter.wait()
    r = requests.get(url, **kwargs)
    if r.status_code == 429:
        time.sleep(30)                        # the mandated penalty
        return get(url, **kwargs)
    r.raise_for_status()
    return r

Deliberately targeting four rather than five leaves room for clock imprecision and is worth the small loss in throughput.

The limit is per base, not per token, so several integrations against the same base share it. A scheduled sync running flat out will rate limit an interactive tool your team is using, and neither will show an obvious cause.

Pagination and reading a whole table

Records come back 100 at a time with an offset token for the next page. Combined with the rate limit, a 50,000-record table takes 500 requests, which at four per second is a little over two minutes of continuous polling.

def all_records(base, table, token, **params):
    url = f"https://api.airtable.com/v0/{base}/{table}"
    headers = {"Authorization": f"Bearer {token}"}
    offset = None

    while True:
        q = dict(params, pageSize=100)
        if offset:
            q["offset"] = offset

        data = get(url, headers=headers, params=q).json()
        yield from data["records"]

        offset = data.get("offset")
        if not offset:
            return

Two ways to make this much faster, both of which reduce the number of requests rather than the rate.

Request only the fields you need, with repeated fields parameters. This does not reduce the request count but substantially reduces payload size and processing time on both ends.

Filter server-side rather than client-side. A filterByFormula expression evaluated by Airtable means fewer pages to fetch, and that is a direct reduction in requests.

params = {
    "fields": ["Name", "Status", "Due"],
    "filterByFormula": "AND({Status} != 'Done', IS_AFTER({Due}, TODAY()))",
    "sort[0][field]": "Due",
    "sort[0][direction]": "asc",
}

The formula language is Airtable’s own, not SQL, and it is fussy about quoting: field names in curly braces, string values in single quotes. A malformed formula returns a 422 rather than being ignored, which is at least a clear failure.

Writing, and the batch limit

Writes are capped at 10 records per request, which combined with five requests per second gives a practical ceiling of roughly 50 records written per second under ideal conditions.

def create_records(base, table, token, records):
    url = f"https://api.airtable.com/v0/{base}/{table}"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }
    for i in range(0, len(records), 10):
        chunk = records[i:i + 10]
        body = {
            "records": [{"fields": r} for r in chunk],
            "typecast": True,          # coerce strings into select options etc
        }
        limiter.wait()
        requests.post(url, headers=headers, json=body).raise_for_status()

The typecast flag is worth understanding rather than copying. It lets Airtable coerce values, including creating new options in a single-select field when it encounters an unknown value. Convenient during import, and a good way to end up with a select field containing forty near-duplicate options if your input data is inconsistent.

Importing tens of thousands of records through the API is not the right tool. It will take a long time and consume your rate limit. Use a CSV import in the interface for bulk loading and keep the API for incremental changes.

The limits that decide whether Airtable fits

These are product limits rather than API limits, and they are the ones that determine whether building on Airtable is a good idea for your use case.

  • Records per base are capped by plan, commonly 1,500 on free tiers through to 500,000 at the top. Approaching the cap degrades performance well before you reach it.
  • Attachments count against storage quotas, and attachment URLs returned by the API expire, so they cannot be used as permanent image sources.
  • There are no transactions. A multi-record write that fails partway leaves partial data with no rollback.
  • There is no unique constraint. Deduplication is your responsibility, and concurrent writers will create duplicates.
  • Formula and rollup fields are computed by Airtable and are read-only through the API.
  • Webhooks exist and are considerably better than polling, so reach for those before writing a scheduled sync.

The honest summary: Airtable is excellent as a database that humans edit, and it is not a transactional application database. The pattern that works well is treating it as the interface your team already likes while your application keeps its own store, syncing between them deliberately.

The pattern that goes wrong is making Airtable the primary datastore for a customer-facing application, then discovering the rate limit under real traffic with no way to raise it.

How this fits the rest of the stack

An integration against a rate-limited API needs somewhere to run on a schedule, a place to keep its own state so it can resume rather than restart, and logs showing what was synced and what was throttled. Webhook receivers additionally need a public HTTPS endpoint. Those are ordinary service and database line items, and the RunxBuild hosting calculator shows them side by side.

Useful related references:

FAQ

What is the Airtable API rate limit?

Five requests per second per base. Exceeding it returns a 429 and requires a 30-second wait before further requests are accepted, so pacing requests is far more effective than bursting and backing off.

How many records does the Airtable API return per request?

100 for reads, with an offset token for the next page. Writes are capped at 10 records per request, which gives a practical ceiling of roughly 50 records written per second.

Should I use table names or table IDs in the Airtable API?

Table IDs beginning with tbl. Names are editable by anyone with base access, so an integration keyed on the name breaks silently when someone renames the table.

Can I use Airtable as a production database?

As a datastore humans edit, yes. As a transactional application database, no. There are no transactions, no unique constraints, a hard rate limit that cannot be raised, and per-plan record caps that degrade performance as you approach them.

Do Airtable attachment URLs expire?

Yes. URLs returned by the API are temporary and cannot be used as permanent image sources. Download and store the file yourself if you need a durable reference.

#Airtable API#REST API#Rate Limits#Pagination#Integrations