DEV Community

Cover image for I Ran 89,479 WhatsApp Messages Through WAHA. Twilio: $604.
אחיה כהן
אחיה כהן

Posted on

I Ran 89,479 WhatsApp Messages Through WAHA. Twilio: $604.

Last month my WhatsApp stack moved 89,479 messages. I got no invoice for any of them.

That is not a brag, it is the setup for an honest accounting. Because "self-hosting is cheaper" is the least interesting sentence in infrastructure, and it is usually said by someone who has never been paged at 7am by a bot that went quiet at 2am. I want to put a real number on both sides of that trade: the money Twilio would have charged, and the money self-hosting quietly takes back.

All the numbers below were pulled or fetched on August 27, 2026. The rate cards move quarterly, so check yours.

The traffic, measured rather than estimated

Five WhatsApp inboxes, bridged from WAHA into a self-hosted Chatwoot. Thirty days:

messages
Total 89,479
Inbound (from users) 45,563
Outbound (from us) 43,916

Most benchmarks stop here, multiply by a per-message rate, and publish. That answer is wrong, because Meta does not charge per message. It charges per template sent outside an open customer service window. Multiplying my full 89,479 by a template rate overstates the Meta line by about 3x. Multiplying just the outbound half still overstates it by about 1.5x.

Since November 1, 2024 non-template messages are free. Since July 1, 2025 utility templates answering a user inside an open 24-hour window are also free. So the only line that costs money is the outbound message that goes out when nobody has written to you in the last day.

Which means the number you actually need is not "how many messages," it is "how many outbound messages had no inbound message from that contact in the preceding 24 hours."

The query that produces the real bill

Here it is against Chatwoot's schema. It uses a window function rather than a correlated NOT EXISTS, because on a messages table of any size the correlated version will happily eat your connection pool.

WITH src AS (
  SELECT m.conversation_id, m.created_at, m.message_type
  FROM messages m
  WHERE m.inbox_id IN (27, 23, 46, 50, 48)   -- your WhatsApp inboxes
    AND m.created_at > now() - interval '33 days'
    AND m.message_type IN (0, 1)             -- 0 = incoming, 1 = outgoing
), w AS (
  SELECT *,
    max(created_at) FILTER (WHERE message_type = 0) OVER (
      PARTITION BY conversation_id ORDER BY created_at
      ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
    ) AS last_in
  FROM src
)
SELECT
  count(*) FILTER (WHERE message_type = 1) AS outbound,
  count(*) FILTER (WHERE message_type = 1
        AND last_in IS NOT NULL
        AND created_at - last_in <= interval '24 hours') AS inside_window,
  count(*) FILTER (WHERE message_type = 1
        AND (last_in IS NULL
             OR created_at - last_in > interval '24 hours')) AS outside_window
FROM w
WHERE created_at > now() - interval '30 days';
Enter fullscreen mode Exit fullscreen mode

Two details worth stealing: pull 33 days of history but count only the last 30, otherwise messages near the boundary look like they have no preceding inbound and you overstate your bill. And ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING is what keeps a message from counting itself as its own window opener.

My result:

 outbound | inside_window | outside_window
----------+---------------+----------------
    43916 |         14314 |          29602
Enter fullscreen mode Exit fullscreen mode

So of 43,916 outbound messages, 29,602 (67%) would have been billable templates. The other third rode free inside an open conversation.

What that costs on Twilio

Twilio's WhatsApp pricing is two layers. Its own fee is $0.005 per message, inbound or outbound, and that one applies to all 89,479. On top sits Meta's template fee, passed straight through, which varies by the recipient's country calling code.

I'm in Israel. Meta's USD rate card effective July 1, 2026:

Template category Israel UK Germany India
Marketing $0.0353 $0.0635 $0.1365 $0.0118
Utility $0.0053 $0.0220 $0.0550 $0.0014
Authentication $0.0053 $0.0220 $0.0550 $0.0014

Israel is a cheap market for utility and a mid market for marketing. Germany is over ten times Israel's utility rate. If you are benchmarking, your country's row is the whole story. A US-centric blog post about WhatsApp costs is nearly useless to you.

The month, priced out:

Twilio handling   89,479 × $0.005   = $447.40
Meta (utility)    29,602 × $0.0053  = $156.89
                                    ---------
                                      $604.29

Meta (marketing)  29,602 × $0.0353  = $1,044.95
                                    ---------
                                    $1,492.35
Enter fullscreen mode Exit fullscreen mode

$604 if every billable template is a utility template. $1,492 if they are marketing. The gap between those two numbers is not a pricing question, it is a template-categorization question, and Meta decides the category, not you.

Scaled to the 10,000-messages-a-month shape people usually ask about, holding my ratios (49% outbound, 67% of that outside the window): $67.53 utility, $166.77 marketing.

The other side of the ledger changed in June, quietly

If you last looked at WAHA a year ago, you priced it wrong. WAHA used to sell a "Plus" license for the features anyone running it in production actually needs. In release 2026.6.1, announced June 21, 2026, every Plus feature moved into the free Core image and the tiers collapsed into a single optional $5/month Community subscription. The project's own words: "100% free and open source, with no limits on messages or time, and no license expiration."

So the software line is now genuinely $0.

The hardware line: WAHA is running on a 2 vCPU / 4 GB / 40 GB x86 VPS. It is not alone on that box: Redis and a second internal service share it. While carrying those 89,479 messages, the machine's load average was 0.05, 0.22, 0.23. This workload is not compute-bound, it is session-bound. Whatever your provider charges for the cheapest 4 GB instance, single digits a month, it does not move the comparison. Round the infra to $10 and self-hosting is still about 60× cheaper than the utility-rate scenario.

Which is exactly the point at which an honest post has to stop being a sales pitch.

What self-hosting actually charges you

It bills in incidents, not invoices. Here is last week's, in full.

On August 23 I rotated WAHA_API_KEY. The rotation updated /opt/waha/.env and the nginx snippet that injects the header. What it did not update was a second vhost, an internal tailnet-only copy created weeks earlier by duplicating the public one, where the key had been written as a literal instead of the $waha_api_key variable.

Result: every GET /api/files/… over the internal path started returning 401. Bots that download an image, OCR it, and act on the contents stopped being able to fetch anything. Of the three code nodes involved, one threw and killed its run outright; the other two caught the error and skipped silently, so those pipelines produced output — just output with no images in it. The public vhost was untouched, so every check that went through the domain came back green.

The failure surfaced the next morning, because nobody had sent a photo overnight. A one-line fix, roughly twelve hours of quiet wrongness, and a monitoring path that was structurally incapable of seeing it.

Twilio would have absorbed that entire class of problem. That is what the $604 buys. Not messages. The absence of a category of 2am.

And there is a risk that no amount of care removes. WAHA drives a real WhatsApp Web session. Its own homepage says it plainly: "WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe." A banned number is not a support ticket, it is a dead asset. That is a business risk, not a technical one, and it does not belong in a cost table. It belongs in whatever conversation you have before you build.

The line I actually draw

After enough of these deployments, the rule I use with clients is not about volume at all:

  • The number is the business. A clinic's only phone line, a store's published contact. Official API, pay Twilio. The ban risk is unpriceable.
  • Internal, operational, or a secondary channel. Courier groups, staff notifications, an intake line that can be replaced in an afternoon. WAHA. The savings are real and the downside is survivable.
  • Marketing blasts at any volume. Official API, and check the marketing rate for your country before you promise anyone a budget. At $0.0353 a message in Israel and $0.1365 in Germany, the same campaign has wildly different economics depending on who receives it.

Volume is the wrong axis. Replaceability is the right one. This is roughly the split behind the pricing I quote Israeli businesses for WhatsApp automation: the channel decision comes first, and the bill follows from it.

One more thing worth knowing: Meta has further pricing changes landing October 1, 2026, covering service and utility messages. Whatever you calculate today has a shelf life of about five weeks.

Run the query

The number that decides this for you is not 89,479 and it is not $604. It is your own outside_window count, and it is the one input a vendor pricing page cannot give you.

So: run it against your own inboxes and post the three numbers. Outbound, inside window, outside window. I am specifically curious whether the 67% outside-window share holds anywhere else, or whether it is an artifact of how my bots are scheduled. If that ratio is stable across use cases, it is a much better rule of thumb than anything on a vendor pricing page.

Top comments (0)