Our mailing list had 306 subscribers. Not one was a person.

The list had grown on its own for weeks without a single campaign. Grouping the signups by network block took ten minutes and answered it: four datacenters, zero home internet connections. The worse discovery was what our own form had been doing.

Our mailing list had 306 subscribers. Not one was a person.

Our mailing list had 306 subscribers. It had filled up over several weeks on the website of a small studio in Udupi, without a single campaign being sent and without anyone being asked to join. On paper that is the kind of problem you want.

Before the first email went out, we checked where those 306 people had come from.

Not one of them was a person.

The check took ten minutes

We did not begin with the email addresses. An address is easy to invent and hard to judge by eye — plenty of real people have odd-looking ones. We began with the IP address stored against each signup, and grouped those by network block: the part of an address that says who owns the connection it came from.

All 306 signups came from four blocks.

  • 105 from one datacenter network
  • 96 from a cloud hosting range
  • 95 from a second range at the same cloud host
  • 10 from a fourth datacenter network

That is the entire list. Four networks, and every one of them sells servers rather than home or mobile internet. Nobody signs up to a newsletter from a rented server.

Bar chart of where 306 newsletter signups came from, grouped by network block: datacenter network A 105, cloud host range 1 96, cloud host range 2 95, datacenter network B 10, and home internet connections such as Jio, Airtel, BSNL and ACT exactly zero
Four networks account for all 306. The bottom row is the one that settles it: no consumer connection appears even once.

The figure that ended the argument

Grouping by network was suggestive. One number made it certain.

Not a single signup came from Jio, Airtel, BSNL, ACT, or any other consumer connection in India. Zero out of 306.

This website is written for businesses on the Karnataka coast. Its pages are about Udupi, Mangaluru, Karkala and the towns around them. If real people were joining the list, most of them would be arriving on a phone, on an Indian mobile network. Instead, exactly one address out of 306 even looked Indian.

A list can have odd entries. A list cannot have zero ordinary ones.

They arrived during office hours, to the minute

The signups using throwaway domains landed between 10:35 and 10:43 in the morning. Not most of them — that window, day after day.

People do not behave like that. A scheduled job does. Somewhere a script was running once a day, and our form was one line in its list of things to submit.

Around 58% of the addresses were not invented at all. They were real work addresses at American companies — a commercial property firm, a city transport authority, a manufacturer — the kind of list that gets scraped from public web pages and resold. Which raises the question of what those addresses were doing in our database, and the answer turned out to be the actual problem.

The list was not the damage

Here is what our signup form did, every time anyone posted to it.

First, it saved the address as an active subscriber straight away. No confirmation, no click, no check. Second, it sent an email to whatever address had been submitted.

Nothing verified that the request had come from our own page. So anyone, anywhere, could send our server an address and our server would email it.

Read that again with the scraped corporate addresses in mind. Roughly 300 strangers received an email from our domain that we never chose to send them. To them it was unsolicited mail from a company they had never heard of. Enough of those and mail providers begin treating everything from the domain as suspect — including the quotes and invoices the studio actually needs to reach clients.

The fake numbers on the dashboard were embarrassing. The open form was the real fault, and it had been sitting there in plain sight.

Four fixes, cheapest first

We put four layers in, in order of cost. It is worth being honest about which ones did the work.

A honeypot field, which was already there and achieved nothing. The usual advice is to add a hidden field that humans leave empty and bots fill in. It only helps against something that loads your page and fills in the form. A script posting directly to the endpoint never sees the form at all. Ours had been in place the whole time the list was filling up.

A signed token, which is the one that closed it. The form now carries a short code that our server generates and signs, and that is only accepted between two seconds and two hours old. Post without a valid code and the request is refused. A script that has never loaded the page cannot produce one, and the two-second floor means a machine cannot fill and submit faster than a person could.

Rate limits, per network as well as per address. Three signups an hour from one address, eight a day from one network prefix. The second limit is the one that matters: the bots hitting us rotated through 109 different addresses that all sat inside 20 prefixes. A per-address limit alone would have barely slowed them down. Anyone limiting by IP address only, on a modern IPv6 network, is counting the wrong thing.

Double opt-in, so a bad signup can never receive mail. A new row is saved as pending, with a token. It becomes a subscriber only when someone clicks the link in the confirmation email. Campaigns are sent to confirmed rows and nothing else. Even if something gets past the first three layers, it cannot be emailed and it cannot be counted.

Then we tested it the way an attacker would: a bare post to the endpoint with no token. It returns a refusal and stores nothing.

What to check on your own site this week

If you have a newsletter box, an enquiry form or a callback request anywhere on your website, this is worth twenty minutes.

  • Does your form store the IP address of each submission? If it does not, add that first. Without it you cannot tell a customer from a script.
  • Group your signups by network block, not by address. Bots rotate addresses and keep the network.
  • Look for the ordinary ones. Count how many came from Jio, Airtel, BSNL or another home connection. If the answer is none, you do not have subscribers.
  • Look at the clock. Real signups scatter across the day. Scripts run at the same minute.
  • Then check the important one. Does your form send an email to the address that was typed in, before anybody confirms it? If it does, your website can be used to send mail to strangers in your name. Fix that before you worry about the list.

We deleted all 306

Every row is gone, with a copy kept in case any of it ever mattered. The list reads zero.

Zero is the honest number, and it is worth more than 306. A list of people who never asked to hear from you cannot buy anything, cannot be reported on truthfully, and quietly damages your ability to send mail to the people who did ask. We would rather build it back one real reader at a time.

If your subscriber count has been climbing without you doing anything to earn it, that is not growth. It is worth ten minutes to find out what it actually is.

← All articles Start a project
Whirl Designs assistantAnswers from this site · not a person
Ask about what the studio builds, how a project runs, or what would suit your business. I answer from this site, and I can pass you to the team any time.