Twenty One Media
automationMay 20, 2026

Vercel Killed Our Airtable Writes (And We Didn't Notice)

We've been fixing our lead pipeline in layers. First, n8n's execution quota silently killed every Airtable write for a week. So we replaced the n8n hop with direct Airtable API calls, inline in each route handler. That seemed like the right fix.

There was a second bug hiding inside the first one.

How We Wired It (Wrong)

When we wrote the direct Airtable calls, we followed a pattern that felt reasonable. Email sends are critical: if Resend fails, we return 502 and the visitor sees an error. Airtable writes are best-effort: if the CRM record fails, the visitor should still get their content and we can recover the lead later from PostHog events.

So we structured it like this:

const [userResult, notifyResult] = await Promise.all([sendUser, sendNotify]);

recordLead.then((r) => {
  if (!r.ok) console.error("[lead-capture] Airtable capture failed:", r.reason);
});

The logic: await the emails, then kick off the Airtable write as a background task so it doesn't add latency to the response.

This works fine on a long-lived server process. Node keeps running after the response is sent, so the .then() callback has time to resolve.

It does not work on Vercel serverless.

What Serverless Actually Does

Vercel's serverless functions are single-invocation processes. When you call return Response.json(...), the runtime terminates. Any promises that haven't resolved yet get dropped, no error, no log, no record.

Our recordLead.then() was kicking off the Airtable write and then immediately handing back the response. Vercel shut down the function. The write never finished.

We had moved from "n8n might miss writes" to "we will definitely miss writes, every time." Just in a different place in the stack.

The Fix

Pull recordLead into the same Promise.all as the email sends:

const [userResult, notifyResult, leadResult] = await Promise.all([
  sendUser,
  sendNotify,
  recordLead,
]);

if (!leadResult.ok) {
  console.error("[lead-capture] Airtable capture failed:", leadResult.reason);
}

All three calls fire concurrently. The function waits for all three before returning. If Airtable is slow, the response is slightly slower. If Airtable fails, we log it and move on. But the write has a real chance to complete before Vercel shuts down the function.

We applied this fix to all five route handlers: /api/lead-capture, /api/audit-request, /api/skill-clone, /api/toolkit-signup, and /api/operations-engine-lead.

The Broader Point

"Fire-and-forget is fine for non-critical work" is true on traditional servers. On serverless, it is a trap. Any work that needs to complete before the function exits has to be awaited, even if that work is optional from the user's perspective.

Concurrency is still the right approach here. We are not adding latency by making Airtable sequential: all three calls fire at the same time. We are just ensuring the function does not exit until each one has had a real chance to finish.

If you are on Vercel and you have any .then() or .catch() callbacks that fire after your response, check whether that work actually needs to complete. If it does, await it. If it truly does not need to complete in this invocation, find a proper mechanism like a queue or background job. Leaving it as an un-awaited promise is a guarantee it will fail silently, and you will not know until you notice the numbers stopped moving.