TL;DR: WhatsApp sends both real messages AND status updates (delivered/read) to the same endpoint. On n8n Cloud, every status update counts as a full workflow execution — burning 60-80% of your plan on noise. If you’re using the native WhatsApp Trigger node, there’s a simpler fix (see below). But if you’re using a generic Webhook node and need to keep it that way, a 30-line Cloudflare Worker as a pre-filter solves this for free. Setup takes ~20 minutes.
First: are you using the WhatsApp Trigger node or a generic Webhook?
If you’re using the native WhatsApp Trigger node, you may be able to solve this without any external service. In the trigger node settings, set Trigger On = Messages and leave Receive Message Status Updates empty (don’t select All, Sent, etc.). This filters status updates at the trigger level. Check whether this reduces your billed executions — some users report it does.
If you’re using a generic Webhook node — either because you need more control over the raw payload, you’re managing multi-tenant setups, or you simply set it up before the WhatsApp Trigger node existed — this post is for you. The generic Webhook node has no built-in filter, and adding an IF node after it doesn’t help: n8n counts the execution the moment the webhook fires, regardless of what comes next.
The problem
If you’re running a WhatsApp bot on n8n Cloud, you’ve probably noticed that your execution count grows much faster than expected. Here’s why:
When you subscribe to the messages field in the WhatsApp Cloud API webhook, Meta sends two completely different types of payloads to the same URL:
Type 1 — A real incoming message (what you actually want):
json
{
"object": "whatsapp_business_account",
"entry": [{
"changes": [{
"value": {
"messages": [{
"from": "573193262632",
"type": "text",
"text": { "body": "Hello bot" }
}]
}
}]
}]
}
Type 2 — A status update (delivery/read receipts for messages YOUR bot sent):
json
{
"object": "whatsapp_business_account",
"entry": [{
"changes": [{
"value": {
"statuses": [{
"status": "delivered",
"recipient_id": "573193262632"
}]
}
}]
}]
}
For every message your bot sends, you’ll typically receive 2-3 status webhooks (sent → delivered → read). In an active bot, status updates can be 3-5x more frequent than actual messages.
Why are some people using a generic Webhook instead of the WhatsApp Trigger?
Valid reasons include:
-
You set up the flow before the native WhatsApp Trigger node existed or before you knew about it
-
You need access to the raw payload (headers, signatures, full body) without the abstraction the trigger node adds
-
You’re building a multi-tenant setup where one webhook handles multiple WhatsApp numbers
-
You’re integrating with a custom Meta app setup that doesn’t map cleanly to the trigger node’s credential model
Whatever the reason: if you’re on a generic Webhook node, read on.
Why filtering inside n8n isn’t enough
The obvious first attempt is to add an IF node right after the Webhook node and discard payloads that don’t contain messages. This has been suggested multiple times in the community but it doesn’t save executions — n8n counts the execution the moment the webhook fires, regardless of what comes next.
The sub-workflow approach (routing real messages to a child workflow) also doesn’t help — it actually increases total executions for real messages, as pointed out in that same thread.
There is no way within n8n Cloud to prevent a generic Webhook trigger from counting against your plan, even if the workflow exits at the first node.
Why you can’t fix it in Meta either
I checked the WhatsApp Cloud API docs and tested the dashboard thoroughly: there is no granular subscription that lets you receive messages without also receiving statuses. They are bundled under the same messages field. Unsubscribing from messages would break the bot entirely.
The solution: a Cloudflare Worker as a pre-filter
The fix is to put a tiny piece of code between Meta and n8n that:
-
Receives all webhooks from Meta
-
Inspects the payload
-
Forwards to n8n only if it’s a real message
-
Responds
200 OKto Meta for status updates and discards them
Cloudflare Workers is perfect for this:
-
Free tier: 100,000 requests/day. You will never hit this with a WhatsApp bot.
-
Latency: ~10-50ms added. Imperceptible.
-
Setup: 20 minutes from zero account to working filter.
Step-by-step setup
1. Create a Cloudflare account
Go to https://dash.cloudflare.com/sign-up. No credit card required. Skip the “add a website” prompt.
2. Create a Worker
Navigate to Workers & Pages → Create → Create Worker. Name it something like whatsapp-filter.
After Cloudflare deploys the default “Hello World”, click Edit code.
3. Paste the filter code
Replace the default code with this. Update the N8N_WEBHOOK constant with your own n8n webhook URL:
javascript
export default {
async fetch(request, env) {
const N8N_WEBHOOK = 'https://YOUR-INSTANCE.app.n8n.cloud/webhook/YOUR-WEBHOOK-ID';
const VERIFY_TOKEN = env.VERIFY_TOKEN;
// 1. Initial verification from Meta (GET request)
if (request.method === 'GET') {
const url = new URL(request.url);
const mode = url.searchParams.get('hub.mode');
const token = url.searchParams.get('hub.verify_token');
const challenge = url.searchParams.get('hub.challenge');
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
return new Response(challenge, { status: 200 });
}
return new Response('Forbidden', { status: 403 });
}
// 2. Real webhook (POST request)
if (request.method === 'POST') {
const body = await request.json();
const hasMessage = body?.entry?.[0]?.changes?.[0]?.value?.messages;
if (hasMessage) {
// Real message → forward to n8n
return fetch(N8N_WEBHOOK, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': request.headers.get('X-Hub-Signature') || '',
'X-Hub-Signature-256': request.headers.get('X-Hub-Signature-256') || ''
},
body: JSON.stringify(body)
});
}
// Status update → respond 200 and discard
return new Response('OK', { status: 200 });
}
return new Response('Method not allowed', { status: 405 });
}
};
Click Deploy. You’ll see a warning that VERIFY_TOKEN is not defined — that’s expected, we configure it next.
4. Configure the Verify Token as a Secret
Go to your Worker’s Settings → Variables and Secrets → Add.
-
Type: Secret (not Text — must be encrypted)
-
Variable name:
VERIFY_TOKEN -
Value: any random string you generate (e.g.,
whatsapp_filter_a8f3d92b1e7c). Save it; you’ll need it in Meta.
Save and redeploy.
5. Test the Worker before touching Meta
This is critical — verify the Worker works before you change anything in production.
Test 1 — valid token (should return the challenge):
Open this URL in your browser, replacing the values with yours:
https://YOUR-WORKER.workers.dev/?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=test12345
Expected response: test12345
Test 2 — invalid token (should block):
https://YOUR-WORKER.workers.dev/?hub.mode=subscribe&hub.verify_token=wrong&hub.challenge=test12345
Expected response: Forbidden
If both work, you’re safe to proceed.
6. Update Meta’s Callback URL
Keep your old n8n webhook URL handy — if anything fails, you can revert in 30 seconds.
In Meta for Developers → your app → WhatsApp → Configuration → Webhook → Edit:
-
Callback URL: your Worker URL (e.g.,
https://whatsapp-filter.YOUR-USERNAME.workers.dev) -
Verify Token: the same value you set as the secret in step 4
Click Verify and Save. Meta will GET your Worker, the Worker validates the token, and Meta saves it.
7. Verify end-to-end
-
Open n8n → Executions (left sidebar, the global list — not the per-node panel).
-
Send a real message to your bot from your personal WhatsApp.
-
Wait 30 seconds.
You should see:
-
One new execution (your message) -
No extra executions for delivered/readstatus updates
Measuring the savings
After 24-48 hours, check your Worker metrics:
Cloudflare Dashboard → your Worker → Metrics
You’ll see total requests received vs. requests forwarded to n8n. The difference is your savings.
In my case (a bot with moderate traffic), the Worker filters out roughly 70% of incoming requests. That’s a 70% reduction in n8n executions for the WhatsApp workflow.
When NOT to use this
-
You can switch to the native WhatsApp Trigger node: that’s the simpler path. Check first if its built-in status filter actually reduces your billed executions. If it does, use it and skip the Worker entirely.
-
Low-volume bots (under 100 messages/day): the savings might not justify the extra moving part. Just live with it.
-
You actually need the status updates in your workflow (e.g., to mark messages as read in a CRM): modify the Worker to forward statuses to a different, simpler n8n webhook instead of dropping them.
-
You’re self-hosting n8n: executions don’t cost you anything there, so the only benefit is cleaner execution history. Probably not worth it.
Limitations
-
The Worker adds ~10-50ms of latency. Negligible for chat, but worth knowing.
-
If the Worker goes down (extremely rare on Cloudflare), all webhooks fail — including real messages. Cloudflare’s SLA is excellent but it’s still one more dependency.
-
If you re-deploy n8n with a new webhook URL, you also need to update the Worker code.
A request to the n8n team
This is a workaround for what is essentially a billing-model issue. It would be incredibly valuable if n8n Cloud could implement one of the following:
-
Lite executions for workflows that exit at the first IF/Filter node (count at, say, 1/10th the cost).
-
Webhook-level filters in the trigger node config, so the IF logic happens before the execution counter increments.
This pattern (webhook providers that don’t allow granular subscriptions) is extremely common — Stripe, Shopify, GitHub, and Telegram all do similar things. A platform-level solution would help many users.
(Filed as a feature request here: [link to your feature request])
Hope this helps someone else hitting the same wall. Happy to answer questions in the comments.
-– David Cancino — Supremotech

