Dev.to AI 🤖 Ai 👁 0 📖 5 min read

Rate-Limit Your n8n Webhook After Signature Verification, Not Before

Rate-Limit Your n8n Webhook After Signature Verification, Not Before This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is from real templates I sell and support —

Rate-Limit Your n8n Webhook After Signature Verification, Not Before

This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is from real templates I sell and support — the mistakes are mine, the fixes are tested.

The comment that sent me back to my workflows

Last week's article treated the n8n webhook URL like a password: it leaks through screenshots, it has no second factor by default, and the real fix is HMAC signature verification as the first node — fail closed, before any side effect.

A reader (rushdeploy) left the obvious next comment: rate-limit the webhook too, so a leaked URL can't be hammered while you rotate the path.

He was right. And it surfaced an ordering detail that changes how the whole thing has to be built: the limiter goes after verification, not before. Get that backwards and your rate limiter becomes the attacker's weapon.

Why verify-then-limit

Think about what the limiter is actually defending against. The attacker holds a leaked URL and can fire arbitrary payloads at it. If the rate limiter runs first:

  1. The attacker burns through the bucket with garbage requests.
  2. Legitimate callers — your client's store sending real order events — start getting dropped.

You've just handed the attacker a throttle against your own traffic. The limiter is doing the DoS for them.

Run verification first and the math flips: garbage requests die at the signature check, cost nothing from the bucket, and the limiter only ever sees requests that already proved they're from a real caller. Same fail-closed instinct as the rest of the pipeline — distrust first, allocate resources only to the trusted.

Where the limiter lives in n8n

The Webhook node has no built-in rate limiting, so in practice it's one of three places:

1. Cloudflare in front of your n8n domain. Free-tier per-IP rules. Cheapest option if you already proxy through it. Downside: it can't see verification state — Cloudflare runs before n8n's signature check, so this is limiter-first by architecture. Fine as a blunt outer shield against volumetric floods, but it's not your per-sender limiter.

2. A small middleware or edge function with a Redis counter keyed by webhook path. Sits after verification. The only one of the three that stays correct across multi-instance setups — the counter is shared, not per-process. This is what I reach for in production.

3. Inside n8n itself. Count per-sender in static data or a DB table, and drop over-limit executions. Fine on a single instance; gets racy the moment you scale out (two instances both read 47, both write 48).

Whatever you pick, the invariant is the same: no bucket is touched until the signature checks out.

// Code node: verify FIRST, count SECOND
const crypto = require('crypto');
const secret = $env.WEBHOOK_SIGNING_SECRET;
const rawBody = $json._rawBody ?? JSON.stringify($json.body);
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = $json.headers['x-signature-256'] ?? '';
const ok = received.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) throw new Error('webhook signature mismatch — rejecting'); // bucket untouched

// ...only now: check the rate-limit counter, keyed on the verified sender...

Key the bucket on identity, not IP

Once the limiter sits after verification, you get to use something you didn't have before: a trustworthy sender identity. HMAC-verified webhooks usually hand you one for free — the provider's delivery ID, an API key header, a tenant id. Bucket per that.

Keying by IP brings the shared-fate problem back in another form. NAT'd offices put dozens of users behind one address; serverless egress ranges do the same at cloud scale. One bad actor behind the same egress burns the quota, and every legitimate caller behind it gets throttled. You've traded one DoS vector for another.

Rule of thumb: if the request survived verification, you know who sent it — so limit them, not their neighborhood.

The rotation wrinkle nobody plans for

rushdeploy's original point was that rate limiting buys you time while you rotate the path. But rotating correctly has its own ordering problem.

Rotate the path and the secret together. A new path with the old secret is theater — every previously captured signed payload still verifies against the new URL.

The wrinkle: rotating the secret alone needs a grace window where both old and new verify, because in-flight legitimate events are still signed with the old secret. During that window every captured payload is still replayable, so the grace window doesn't actually stop replays — retiring the path does. Which means the old path has to die immediately when you detect the leak, not at the end of the grace window.

The order that works:

  1. Kill the old path now. New path goes live, old path 404s.
  2. Dual-verify during the grace window. Both secrets accepted, so in-flight events survive.
  3. Drop the old secret after. The captured payloads finally stop verifying.

Miss step 1 and you're rate-limiting and dual-verifying against an attacker who can still reach you.

The checklist I run now for every webhook workflow

  1. Signature verified before any side effect, fail closed, timingSafeEqual on the raw body (from last week's article)
  2. Rate limiter runs after verification — no bucket touched by unverified requests
  3. Bucket keyed on verified sender identity (delivery ID / API key / tenant id), never bare IP
  4. Limiter state shared across instances (Redis counter, not per-process memory or racy static data)
  5. On leak: rotate path and secret together; old path dies immediately, dual-verify during the grace window, then drop the old secret
  6. Cloudflare (or equivalent) as a blunt outer shield is fine — but it's not a substitute for the post-verification limiter

Ordering is the whole game with webhooks: verify before side effects, limit after verification, retire the path before retiring the secret. Every layer trusts the one inside it and nothing outside.

I'm building AI Automation Lab — production-ready n8n workflow templates. The free AI Hook Generator shows how I structure real workflows; my open-source templates live at github.com/zhp910318/n8n-ai-automation-lab.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.