Twenty One Media
automationMay 14, 2026

When n8n's Quota Ran Out and Killed Every Form Submission

We've written before about the two jobs every lead form has: send transactional emails to the visitor, and log the lead into the pipeline. The first job fails loudly. The second one fails silently.

We learned that lesson the hard way once. Then we learned it again.

What Happened

Our lead pipeline ran like this: a visitor fills out a form, the API route sends Resend emails, then it fires a webhook to an n8n cloud workflow. n8n receives the payload, writes to Airtable, and pings Slack.

We had five routes wired this way: /api/lead-capture, /api/audit-request, /api/skill-clone, /api/toolkit-signup, and /api/operations-engine-lead.

All five routes kept sending emails without issue. Visitors got their confirmations. We got our notification emails. From every visible signal, the forms were working.

What was actually happening: n8n had hit its monthly execution quota. The workflow stopped running. Every webhook call we fired just bounced. No errors returned, no alerts, no console noise. Airtable stayed frozen at whatever it was before the quota ran out.

We had no idea until we noticed the lead count had flatlined.

Why n8n Was the Wrong Place to Put This

n8n is a great tool. We use it for complex multi-step workflows where visual chaining earns its keep. But using it as a passthrough for a simple Airtable write was adding a dependency we didn't need.

Every lead that came in had to survive: the API route, the network hop to n8n, n8n's execution budget, n8n's own connection to Airtable. Four points of failure when one would do.

Cutting n8n out of this path meant writing a thin captureLead() function that calls the Airtable API directly. One HTTP request, inline, no orchestration layer in between.

The Replacement

The new src/lib/lead-capture.ts does three things:

  1. Checks if the email already exists in Airtable before writing (deduplication via filterByFormula).
  2. Creates the record if it doesn't exist, with fields for name, email, source, path, lead magnet, and signup date.
  3. Never throws and never blocks the response. If the Airtable write fails, we log it and move on. A flaky API call should never stop a visitor from getting what they came for.
export async function captureLead(input: LeadInput): Promise<LeadResult> {
  try {
    const existing = await findByEmail(pat, normalizedEmail);
    if (existing) {
      return { ok: true, existed: true, recordId: existing };
    }
    const recordId = await createRecord(pat, input);
    return { ok: true, existed: false, recordId };
  } catch (err) {
    console.error("[lead-capture] Airtable write failed:", err);
    return { ok: false, reason: err instanceof Error ? err.message : "unknown" };
  }
}

All five routes now call captureLead() directly. The n8n hop is gone.

Adding a Second Source of Truth

We also added PostHog across the stack. The problem with relying only on Airtable for lead visibility is that it puts everything downstream of the server write. If the write fails quietly, you have nothing.

PostHog gives us a second, independent signal. On the client, every successful form submission fires a lead_submitted event with the source and lead magnet. On the server, each API route fires its own named event: lead_captured, audit_request_processed, operations_engine_lead_processed.

Now we have two places to check: Airtable for CRM records, PostHog for event volume. If they diverge, something in the write path is failing. We'll know before it flatlines for a week.

The Broader Pattern

The original n8n webhook architecture made sense when we had one lead form and wanted to build out the pipeline workflow quickly. Once we had five forms and a production site, the abstraction was costing more than it saved. Every new form had to know about the webhook URL. Every submission had to survive an external service's execution budget.

Direct writes are less interesting to look at, but they're easier to debug, cheaper to maintain, and they fail in ways that show up in logs.

We still use n8n for workflows that actually benefit from it: multi-step sequences, conditional branching, scheduled jobs. We just stopped using it as a proxy for things that are one API call.