InAir's Latest Insights on Airtable

Retry Logic for Airtable Integrations: Transient vs. Permanent Errors

Written by Julia Eboli | Nov 6, 2026, 6:53:04 PM


An order system pushes updates into an Airtable base through a middleware worker. On a busy afternoon three things go wrong within the same hour: Airtable returns a 503 on one request, the worker trips the base's rate limit and gets a 429, and one order arrives with a status Airtable's single select doesn't have and, sent without typecast, comes back 422. An integration with no retry policy treats all three the same way. Either it drops them, so the 503 and the 429 lose two orders that would have gone through seconds later, or it retries all of them, so the 422 is resent until the worker gives up, spending requests from a budget the whole base shares.

InAir's guide to operating Airtable inside complex, high-volume system landscapes sets out the rule under Principle 1, Classify Faults Before Taking Action: transient faults go to an exponential backoff loop with jitter, and the integration "halts auto-retries immediately on the first 400-level error other than 429." This post turns that rule into a policy you can implement: which response gets which treatment, how long to wait, when to stop, how to keep retries inside Airtable's rate limits, and where the logic should run.

 

One Question Decides Every Rule

 

Before deciding what to do with a failed request, ask what would happen if exactly the same request were sent again.

If the answer is "it would probably succeed," the failure is transient. The server was briefly unavailable, or the request arrived while the rate limit was exhausted, and nothing about the payload needs to change. Retry it, after a pause.

If the answer is "it would fail the same way," the failure is permanent for that payload. Retrying it spends rate limit and time on a result you already know. It needs a person or a code change, so it goes to the dead letter queue on the first attempt.

There's a third answer that's easy to miss: "every request would fail the same way, not just this one." An expired token or a revoked base permission isn't a problem with one payload. Sending each payload to the dead letter queue would fill it with hundreds of copies of one problem, so the right response is to stop the integration and tell its owner.

Airtable's error codes map onto those three answers:

 

Response

Meaning

Treatment

429 Too Many Requests

A rate limit was exceeded

Retry, after waiting at least 30 seconds

500, 502, 503

Airtable couldn't complete the request

Retry with exponential backoff and jitter

No response (timeout, dropped connection)

Unknown: the write may or may not have landed

Retry with backoff, only if the write is safe to repeat

400, 404, 413, 422

Something about this request is wrong

Dead letter queue, no retry

401, 403

The credentials can't do this, for any payload

Stop the integration and alert its owner

 

Two of those rows need more care than the table can give them.

A 404 can be one payload or all of them. A 404 on an update to a record that someone deleted belongs to that payload alone. If every request starts failing with a 404 or a 403, the integration is usually addressing a table or base that was renamed, removed or no longer shared with its token, and that's a configuration problem in the same class as a 401.

A 422 can also hit every payload. If someone renames a field the integration writes by name, every request fails with 422 from that moment on. The response code says "this payload," but the cause is the integration. The fix is a simple rule on top of the table: if a run of consecutive payloads goes to the dead letter queue with the same error, stop the integration and alert its owner, exactly as for a 401. Five in a row is a reasonable starting point for a steady integration, since five unrelated payloads failing identically is rarely a coincidence.

 

The 429 Rule Is Different From Every Other Retry

 

Airtable's rate limit documentation sets two limits: 5 requests per second per base, and 50 requests per second across all traffic using personal access tokens from one user, with the same 50 applying to a service account. Exceeding either returns a 429, and the documentation says the client "will need to wait 30 seconds before subsequent requests" succeed.

That means a 429 has to be handled in three ways.

  • The wait is fixed at a minimum of 30 seconds. A 2-second backoff after a 429 produces another 429 and spends another request. Wait 30 seconds, plus a few seconds of jitter, before the next attempt.
  • The whole worker pauses, not just the request. Every request the worker sends to that base during the next 30 seconds will fail too, so a worker that keeps sending other payloads while one waits is only adding to the failure count. Run one worker per base and have it process writes one at a time, so a pause on one request is a pause on all of its writes. On a serverless function reading from a queue, that means limiting the function to one concurrent execution, since by default it scales out and runs several at once.
  • Prevention is cheaper than recovery. A worker paced at four requests per second, rather than five, leaves room for other integrations and for the occasional burst. If several integrations write to the same base, they share those 5 requests per second, so agree a share for each rather than letting each assume the full limit.

The 50 requests per second limit is easy to overlook because it's counted per user, not per integration. Every integration that runs on tokens from the same person draws on the same 50, however many bases they write to. Give each production integration its own service account where your plan offers them, so one integration's burst can't push another into a 429.

If the integration uses Airtable’s official JavaScript client, the rate limit documentation notes that the client already has back-off and retry logic built in, so don’t wrap it in a second retry loop, which would multiply the attempts on every 429.

 

A Backoff Schedule That Ends Somewhere

 

For 5xx responses and requests that got no response, retry with exponential backoff: wait about 2 seconds after the first failure, 4 after the second, 8 after the third, doubling each time. Add jitter, a random amount on top of each wait, so that when a brief outage ends, the requests that failed together don't all come back at the same moment. AWS's guide to timeouts, retries and backoff with jitter gives the reason plainly: "If all the failed calls back off to the same time, they cause contention or overload again when they are retried."

Then set a ceiling. Five attempts in total, with waits of roughly 2, 4, 8 and 16 seconds doubled at most by jitter, adds up to about a minute of waiting at most, plus the time each attempt takes. Five attempts that each time out after 20 seconds, or a run of 429s with 30-second waits, can take up to about three minutes, so set the function's own timeout above that. When the last attempt fails, the payload goes to the dead letter queue with the status code of that last attempt and the number of attempts made, so the reviewer can tell an exhausted transient failure from a payload that was rejected outright. The same guide is direct about the alternative: "In almost all cases, our solution is to limit the number of times that the client retries." A failure that's still there after five attempts needs someone to look at it, and the same ceiling applies to 429s: five rate-limit responses in a row mean the base is overloaded for longer than retries can wait out.

InAir's guide to Architecting for the Failure State sets out the dead letter queue table itself: Timestamp, Automation Name, Error Message, Record Link and Resolution Status. For an integration writing from outside Airtable, add Status Code, Attempts and the full payload, since there may be no Airtable record to link to yet.

 

Retrying a Write That May Already Have Landed

 

The riskiest row in the table is the one with no response. A request that times out after Airtable received it may have been written, and a 503, which Airtable describes as "The server could not process your request in time," leaves the same uncertainty. Retrying an update in that situation is usually safe, because writing the same values to the same record twice leaves it the same, unless another system changed the record in between. Retrying a create isn't. If the first attempt landed, the retry creates a second record.

The same AWS guide states the principle: "APIs with side effects aren't safe to retry unless they provide idempotency." Airtable's record endpoints don't document an idempotency key of the kind payment APIs offer, so the integration has to make its creates safe to repeat in another way. The way Airtable does provide is an upsert. Send creates through the update records endpoint with performUpsert, merging on a field that holds the sending system's own ID for the record. If the first attempt landed, the retry finds that record and updates it. If it didn't, the retry creates it. If more than one record already matches, Airtable fails the request rather than guessing, and that payload goes to the dead letter queue, which is where a duplicate that already exists should surface.

 

Where the Logic Can Run

 

The same policy can live in three places, and each one fits it differently.

 

 

A Run a script action is the tightest fit. Airtable's documentation for the action sets the script time limit at 30 seconds, temporarily raised to 120 seconds while Airtable tests a longer limit, with each fetch timing out after 30 seconds and a cap of 50 fetch requests per run. A 30-second wait after a 429 doesn't fit in a 30-second run, and it takes a quarter of a 120-second one. So a script should make one attempt, and on a 429, a 5xx or no response, write the payload to a retry table with the time its next attempt is due, rather than waiting. A scheduled automation then picks up the due retries. The scheduled trigger's shortest interval is 15 minutes, and each run can make at most 50 fetch requests, so retries handled this way are minutes apart and limited in number per run, which suits low-volume integrations that can tolerate the delay.

Make and n8n come with their own retry behavior, and the policy has to account for it rather than duplicate it. InAir's guide to Creating Operational Resilience covers Zapier's and Make's automatic replay. The point here is how that replay lines up with the table above. Make's automatic retries of incomplete executions cover rate-limit, connection and module-timeout errors on an exponential schedule that starts at one minute, and other error types aren't retried automatically by default. That lines up with the transient rows, as long as Make reports Airtable's 429s and 5xx responses as those error types, so check how your module surfaces them before relying on it. n8n's Retry On Fail setting retries a failed node a set number of times with a fixed wait between tries. In both, route Airtable's responses through an error branch that checks the status code, so 422s go to the dead letter queue and 401s and 403s stop the scenario.

A serverless function gives the most control. A function reading from a queue, as in the AWS Lambda and SQS buffer that the Operational Resilience guide recommends for staying under the rate limit, can pace its writes, pause for 30 seconds on a 429 and apply the full policy. This is the version the code below is written for.

 

javascript

const MAX_ATTEMPTS = 5;

const BASE_DELAY_MS = 2000;

const RATE_LIMIT_WAIT_MS = 30000;

const REQUEST_TIMEOUT_MS = 20000;

 

// What to do with a response, decided by status code alone.

function classify(status) {

if (status === 429) return 'wait'; // rate limit: pause, then retry

if (status === undefined || status >= 500) return 'retry'; // no response, or 5xx

if (status === 401 || status === 403) return 'halt'; // every payload would fail

return 'dead-letter'; // 400, 404, 413, 422: this payload

}

 

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

 

// Exponential backoff with jitter: 2-4s, 4-8s, 8-16s, 16-32s.

const backoff = (attempt) => {

const step = BASE_DELAY_MS * 2 ** (attempt - 1);

return step + Math.random() * step;

};

 

// Upserts up to 10 records, matched on Source Event ID, so a retry of a write

// that already landed updates the same record instead of creating a second one.

async function writeWithRetry({ baseId, table, token, records }) {

let status;

let body;

for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {

try {

const res = await fetch(`https://api.airtable.com/v0/${baseId}/${encodeURIComponent(table)}`, {

method: 'PATCH',

headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },

body: JSON.stringify({ performUpsert: { fieldsToMergeOn: ['Source Event ID'] }, records }),

signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),

});

status = res.status;

body = await res.json().catch(() => null);

if (res.ok) return { outcome: 'written', attempts: attempt, body };

} catch (err) {

status = undefined; // timed out or no connection: the write may or may not have landed

body = { error: String(err) };

}

 

const action = classify(status);

if (action === 'halt' || action === 'dead-letter') {

return { outcome: action, status, attempts: attempt, body };

}

if (attempt < MAX_ATTEMPTS) {

await sleep(action === 'wait' ? RATE_LIMIT_WAIT_MS + Math.random() * 5000 : backoff(attempt));

}

}

return { outcome: 'dead-letter', status: status ?? 'no response', attempts: MAX_ATTEMPTS, body, reason: 'retries exhausted' };

}

 

The function decides what happened to one request of up to 10 records. The worker around it acts on the outcome. When a batch comes back 422, none of its records can be counted as written, so the worker resends them one at a time, which the upsert makes safe whatever happened to the batch, and only the record that still fails goes to the dead letter queue. The worker also counts consecutive single-record failures with the same error and stops after five, and on a halt it stops pulling from its queue, leaves the payloads where they are and alerts the integration's owner. Because the worker handles one batch at a time per base, a 429 pause never overlaps with its other writes to that base.

That last step is where the Integration Load framework's failure visibility factor comes in. A retry policy decides what the integration does on its own, and the dead letter queue and the halt alert decide who finds out when it can't.

 

Testing Each Case in a Sandbox

 

Point the integration at a sandbox copy of the base and run one test per row of the table.

  • 429: while the worker is running, use a separate load script to send requests to the same base well above 5 per second. The worker should pause for at least 30 seconds after its first 429, send nothing to that base during the pause, and then complete every payload, with no duplicates.
  • 5xx and no response: these can't be forced on Airtable's side, so set the request timeout to a few milliseconds for one run, or route requests through a test proxy that returns 503. Each payload should be attempted five times on the expected schedule and then reach the dead letter queue with its status code and attempt count.
  • A create sent twice: create a record through the integration, then send the identical payload again. The base should still hold one record, matched on Source Event ID.
  • 422: without typecast, send a value the single select doesn't have, in a batch with nine valid records. The nine should be written and the one invalid record should reach the dead letter queue after one attempt.
  • A systematic 422: rename a field the integration writes, then send several valid payloads. The integration should stop after the fifth consecutive failure and alert its owner. Rename the field back afterwards.
  • 401 and 403: run once with a revoked token and once with a token that has no access to the base. Each should stop the integration on the first attempt, with one alert and no dead letter queue entries.

If your integrations retry everything, or nothing, and you'd like a policy that tells the two apart before the next outage, Schedule a Discovery Call with InAir and we'll set it up with you.