8 min readSignalGate
The SMS Pumping Attack: Every Dashboard Reports Healthy While Your Bill Climbs
SMS pumping: requesting SMS codes that do not break authentication. From this article you will learn what the attack looks like in telemetry, why rate limits and CAPTCHAs don’t fix it, and the one place in the stack where you can still refuse to pay.
- OTP
- A2P SMS
- Bot detection
- Rate limiting
- Fraud engineering
Our founder was a cybersecurity manager at a company that sent over a million SMS per month for user logins. At some point, conversion — SMS sent versus codes submitted — dropped sharply within a day, while the number of sent messages climbed. Every other graph stayed flat. He found out that traffic was simply inflated: most requests were fake, coming from different IPs but the same network.
If you work at a company that sends many SMS on user request, you may have seen that same shape and read it as a product problem. Most likely, it was artificially inflated traffic.
However, it is not that easy to diagnose the problem. First, most cybersecurity defenses are built against people who do send the requested SMS but try to log in to someone else’s account. Under this attack that problem is absent, as no one inserts the code. Most metrics — error rate, latency, traffic graphs — do not reveal the issue. Second, SMS volume does not indicate the attack either: fluctuations in it are normal. The only artifact arrives four weeks later, from accounts payable.
Your authentication system did its job. That is the whole problem.
This is SMS pumping, also called artificially inflated traffic. It works against any public endpoint that turns a request into a paid message: sign-up, login, account recovery, phone verification, delivery-code resend — anything with a resend button. The message itself is the attack.
Who gets paid, and how
But why would anyone request a code to never see it? Because the money comes from the message itself. The attacker gets a fee as part of your SMS bill: a share of what delivery earns, collected somewhere down the chain by a party the fraudster controls or colludes with. The chain is usually complex; you most likely use an aggregator, that in turn uses another regional provider, and only then there is an operator who actually sends OTPs.
Money is made on the delivery, and sometimes even on non-delivery: the message is receipted as if it arrived, and never arrives. It has similar economics to international revenue-share fraud on voice calls, and there is a good HackerNoon article on the premium-number form. This also explains why your provider sees nothing wrong. The party being paid sits far from anyone you hold a contract with, so your provider accepts, routes and delivers in good faith while someone else intervenes down the route.
Even the largest companies struggle to stop it. In a Twitter Spaces discussion in December 2022, Elon Musk claimed that Twitter was being scammed for $60 million a year in fake 2FA SMS messages, not counting North America, with telcos running two-factor texts over and over against bot accounts to run up the tab. His response was to cut off any telco with fraud above 10 percent — which turned out to be 390 of them — and eventually disable SMS-based two-factor authentication for most users altogether.
Try it yourself. We built an interactive simulation of this attack, so you can see what it looks like: signalgate.ai/demo.
The signal is not volume
So how do you detect it? Most teams notice the spike in SMS-sent volume first, but a spike itself can prove nothing. Such spikes can be caused by launches, outage recovery, a mention in media or just a poor deployment that pushes users to retry. Volume can tell you that something has changed, but not whether SMS traffic was inflated. The question that actually matters is: what happened after the send?

So use information from the whole path, not just the endpoint. Carry a durable request_id across five transitions:
otp.requested— when the endpoint accepts an SMS requestotp.sent— when you ask the provider to send itotp.delivered— when the provider’s receipt comes backotp.verified— when someone enters the code- the downstream outcome — whatever your schema calls the sign-up, recovery or delivery the message existed to enable
While logging the send, include information you will need later:
- the destination country where SMS was sent, and the provider where you know it
- the price you were charged for this route
- the endpoint hit and the risk action taken
- privacy-reviewed hashes for device, prefix, session and network (e.g. IP, OS)
Stop reading provider SMS delivery as product conversion. A message can be accepted, routed, delivered and receipted flawlessly while being, in economic terms, entirely fraudulent.
The query to run first
We made a deliberately plain SQL query. It takes one event row per transition and a route price on send events, and it shows which routes are burning money without producing the outcomes the messages were sent for.
-- otp_events: one row per request_id × transition, e.g.
-- (request_id, event_type, created_at, country_code, carrier, route_price_usd, ...)
-- 'signup.completed' stands in for whatever your schema calls the downstream outcome.
WITH corridor_hour AS (
SELECT date_trunc('hour', created_at) AS hour,
country_code,
carrier,
count(*) FILTER (WHERE event_type='otp.sent') AS sends,
count(DISTINCT request_id)
FILTER (WHERE event_type='otp.verified') AS verifies,
count(DISTINCT request_id)
FILTER (WHERE event_type='signup.completed') AS outcomes,
coalesce(sum(route_price_usd)
FILTER (WHERE event_type='otp.sent'), 0) AS sms_cost
FROM otp_events
WHERE created_at >= now() - interval '7 days'
GROUP BY 1, 2, 3
)
SELECT hour, country_code, carrier, sends, verifies, outcomes,
round(verifies::numeric / nullif(sends, 0), 4) AS verify_rate,
round(sms_cost / nullif(outcomes, 0), 2) AS cost_per_outcome
FROM corridor_hour
WHERE sends >= 100
ORDER BY (outcomes = 0) DESC, sms_cost DESC, cost_per_outcome DESC NULLS FIRST;
-- Two caveats:
-- 1) verified and outcome events must carry the send's country and carrier, or they
-- fall into a NULL group and an attacked corridor can look clean;
-- 2) a send at 10:59 verified at 11:02 lands in the next hourly bucket --
-- join outcomes to the send hour via request_id before trusting small windows.Cost per outcome is the number to argue about; verify_rate is the cheap proxy for teams that cannot yet join the outcome event back to the send. Note the ordering, too. Corridors with zero outcomes are listed first, and cost_per_outcome sorts NULLS FIRST, because dividing by zero outcomes gives NULL — and the default NULLS LAST would bury the exact signature you are hunting for underneath a healthy route costing a cent.
Beyond the first look: compare each route against its own weekday and hour-of-day typical behavior. A global average is likely to stay normal while one expensive route burns money. Then slice again by prefix, device group, acquisition source, app version, autonomous system and endpoint.
What you are looking for is relational rather than per-request:
- sends rise while verifications do not
- numbers from adjacent ranges turn up across sessions that otherwise have nothing in common
- the same infrastructure wanders between sign-up, recovery, resend and voice endpoints
- timing looks machine-regular even though every individual identity stays politely under your limits
The useful question, in other words, is not “is this IP bad?” but “does this request belong to a population that generates paid sends and no outcomes?”
Why the obvious controls miss
The base defense everyone needs is a rate limit on the same IP. However, it is cheap for a fraudster to rotate through a bulk of them, so this only prevents the most basic attacks. Compound counters cost more to evade:
endpoint × device × phone_prefix × ASN × 5-minute windowYou also want separate, longer windows for the destination number range and for the account itself. The exact combination depends on what you can collect and measure. The point is fixed, though: make rotation cost more than the fee a rotation earns.
CAPTCHA might be a good solution. However, first, it is not free. Second, it is relatively cheap to buy solved CAPTCHAs from major providers — around $3 per thousand solved — while your own side of that trade is not fixed: published per-message rates run from well under a cent in the US to nearly thirty cents in some destinations, and the attacker picks the destination.
Another way is to avoid sending SMS. Yes, just move clients to some other way of authorization: passkeys, authenticator apps, and so on. This solves the problem completely, but many people might lose access to your platform: they don’t have the needed devices, apps, or time to change to new ways, and for first-time users there is almost no substitute. That is why NIST SP 800-63B-4 (§3.1.3.3 and §3.2.9) restricts PSTN out-of-band authentication but does not forbid it; the budget case for migrating away from SMS has been made on HackerNoon before. One more thing: do not fall back to voice calls for untrusted requests. Voice has its own revenue-share fraud problem and needs the same spending controls.
Caps on destination country or prefix might also work; however, you would inevitably lose plenty of real clients if your company is present in the region. So compare the two directly: money saved by not sending, against revenue lost on blocked customers. That leaves two places a cap earns its keep:
- where you have no customers
- as a temporary measure while money is actively leaving
Past that, geography is a poor proxy for intent. Travelers, foreign SIM holders and MVNO customers all face login trouble from a cap aimed at someone else.
Where you can still refuse

Provider-side intelligence is real and useful. The problem is it does not know two things: what this route costs you, and what happened after the last message you sent down it.
Those controls are worth switching on. Twilio, Infobip and Vonage now block suspected pumping before it is billed, and Twilio’s Lookup SMS Pumping Risk Score is even a pre-send API you call from your own backend. But all of them read network reputation, not your route price or what the last message on that route actually produced. And everything their models pass, you pay for: a refund is a clause in a contract, not a property of the architecture. Once your backend makes the send call, the decision is out of your hands. The only place left to refuse is the moment before it, where you can price the risk against the route before you buy the message.
So what does refusing actually look like? You already have the inputs, because they are the fields you logged on the send: the price of this route, that corridor’s recent cost per outcome, and how this prefix, device and network have behaved across your endpoints. A workable starting policy has three answers rather than two.
Send when the corridor’s trailing cost per outcome sits near its own median and nothing about the request stands out. Challenge — a CAPTCHA, an email step, a short delay — when the corridor has drifted above its own baseline but the request otherwise looks ordinary. A challenge costs a fraction of a message, and what you are buying is information. Hold when the corridor has produced sends and no outcomes across the recent window, or when the same prefix range and network keep appearing on endpoints that should share no traffic at all.
Set those thresholds against each corridor’s own history rather than a portfolio average, for the reason the query already showed. And write the decision onto the send event: a control you cannot measure afterwards is a control you cannot tune.
The boundary is before billing
SMS pumping works because of a simple mismatch. Your application sees an unentered code and calls the journey incomplete. The attacker sees the same event, but for them the message was billed, so the job is done.
What matters is the outcome, not the request. Did this message actually lead to the action it was sent for?
Most OTP dashboards show delivery rates but not cost per completed outcome. If yours is one of them, an attack like this could run for weeks before anyone notices.
Sources
- GSMA definition of artificially inflated traffic, via Ericsson, “AIT: the root cause, the solution and the implication on RCS and Network APIs,” white paper, ericsson.com.
- Twilio, “SMS Traffic Pumping Fraud,” Twilio Support; “SMS Pumping Protection for Programmable Messaging” and “Lookup SMS Pumping Risk Score,” Twilio Docs; Messaging pricing, twilio.com.
- Vonage, “Fraud Defender,” product documentation, on AIT prevalence and provider-side defenses.
- UK National Cyber Security Centre, “AIT fraud: what you need to know,” ncsc.gov.uk.
- Infobip, “Artificially inflated traffic (AIT): causes and solutions,” infobip.com, on pre-billing blocking of AIT.
- NIST SP 800-63B-4, Digital Identity Guidelines (2025), §3.1.3.3 and §3.2.9, on restricted authenticators and PSTN out-of-band authentication.
- Commsrisk, “Elon Musk Says Twitter Lost $60mn a Year Because 390 Telcos Used Bot Accounts to Pump A2P SMS,” 12 January 2023, quoting Musk’s Twitter Spaces remarks of 20 December 2022.
- CAPTCHA solve pricing: 2Captcha customer pricing, public list rates.