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.
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
- The rate limit that shapes everything
- Pagination and reading a whole table
- Writing, and the batch limit
- The limits that decide whether Airtable fits
- How this fits the rest of the stack
- FAQ
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:
- Railway Documentation: The Deploy Story, the Service Graph, the Variables, and the Cost Calculation That Matters
- n8n Redis Node: The Operations, the Connection, the Pub/Sub, and the One Mistake That Blocks the Workflow
- n8n Expressions Documentation: The $json, $node, and $now Variables, the Date Math, and When to Use a Code Node Instead
- Services on RunxBuild
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.