The Singapore Bot Wave: Why Under Attack Mode Is the Wrong Fix for WordPress

If your WordPress analytics suddenly show thousands of visitors from Singapore (and often Lanzhou, China) with zero-second sessions and a near 100% bounce rate, you are not going viral in Southeast Asia. You are being crawled.

This post covers what the data actually says about this traffic, why flipping Cloudflare’s Under Attack mode is the wrong long-term answer, and the layered setup I would put in front of a WordPress origin instead.

What is actually happening

This is not one blog’s bad week. It is a web-wide pattern with public numbers behind it.

  • Cloudflare (June 2026): bots made up roughly 57.4% of traffic over a seven-day window, and Singapore ranked second of all countries for bot share at 76.3%, behind only Gibraltar.
  • Analytics.usa.gov via WIRED (Feb 2026): over a 90-day window, 14.7% of visits to US government websites came from Lanzhou and 6.6% from Singapore, making them the top two sources.
  • Known Agents: founder Gavin King found China and Singapore traffic made up 22% of his own site’s traffic, routed through Tencent’s AS132203. Other operators reported traffic from Tencent, Alibaba and Huawei cloud ASNs.
  • Clicky (web analytics vendor): the surge started around September 2025, and they now describe Singapore as the worst source of spoofed user agents across their network, with over 95% of Singapore traffic blocked by their bot filters.
  • Thales 2026 Bad Bot Report: bots were 53% of global web traffic in 2025, up from 51% the year before.

WordPress itself told WIRED it had received reports of increased traffic from suspected AI bots and scrapers. The leading theory is AI training scrapers, but nobody has publicly confirmed who is running them. I am not going to pretend otherwise.

Why Singapore specifically

Singapore is not where the operators necessarily live. It is where the servers are. Cloudflare’s own framing of its bot-share data is that countries with large data center footprints, like Singapore, Ireland and the Netherlands, show up because that is where automated traffic is hosted, not where it originates.

In practice that means AWS, DigitalOcean, Tencent and Alibaba regions in Singapore. A single DigitalOcean Singapore IP on AS14061 collected 210 community abuse reports for WordPress login attacks in December 2025 alone. Cloud IPs are cheap to spin up and cheap to throw away, which is why blocking individual IPs is a losing game.

Two different problems wearing the same mask

Before touching any firewall setting, split the traffic into two buckets. They need different fixes.

1. Traffic that hits your origin. Real HTTP requests to nginx and PHP. This costs you CPU, PHP workers, database queries and bandwidth. One forum operator reported 149 GB of transfer in 18 days that was not coming from users.

2. Ghost hits in GA4. Multiple analytics professionals report that a large share of the Lanzhou and Singapore traffic never appears in server logs or CDN analytics at all. The bots send data straight to the GA4 measurement endpoint. Some site owners blocked both countries at Cloudflare and GA4 kept recording Lanzhou visits.

No edge rule fixes bucket 2. It never touches your edge. That is a GA4 filtering problem, not a security problem.

Check which one you have:

# Top client IPs hitting the origin today
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

# Map an IP to its ASN and owner (Team Cymru whois)
whois -h whois.cymru.com " -v 203.0.113.10"

If the IPs at the top of that list are Cloudflare ranges, your nginx is not restoring the real client IP. Fix that first (covered below), or every decision you make from these logs is wrong.

Why Under Attack mode is the wrong fix

Under Attack mode works. That is not the issue. The issue is what it costs and what it does not cover.

Cloudflare tells you not to live in it. Their own documentation describes it as a tool for mitigating layer 7 DDoS attacks, meant as one of the last resorts, and warns it temporarily pauses access to your site and affects your analytics. The Singapore wave has been running since late 2025. A last-resort switch left on for months is a misconfiguration.

It punishes every visitor for the bots’ behaviour. Every human gets an interstitial page that needs JavaScript to pass. Your real visitors from India, the US and Europe pay a latency and friction tax because of traffic from one country’s data centers.

It breaks the parts of WordPress that are not page views. Cloudflare documents that challenge pages fail when the client expects a non-HTML response, such as AJAX or fetch requests. On WordPress that means admin-ajax.php, the REST API, WooCommerce cart fragments, and anything else making background calls. Payment gateway webhooks and uptime monitors are not browsers either, so they cannot solve a JavaScript challenge.

It is a blunt instrument for a precise problem. The traffic comes from a short, identifiable list of cloud ASNs. Challenging 100% of your audience to filter a few networks you can name is the wrong trade.

It does nothing for the GA4 ghost hits. Those never reach Cloudflare in the first place.

What to do instead

The goal is to push the decision as close to the edge as possible, scope it as tightly as possible, and make the origin cheap to hit when something slips through.

1. Lock the origin to Cloudflare only

If your origin IP accepts traffic from anywhere, bots can skip Cloudflare entirely and none of the rules below matter. Allow ports 80 and 443 only from Cloudflare’s published ranges at the server firewall:

# Cloudflare publishes its ranges here:
# https://www.cloudflare.com/ips-v4
# https://www.cloudflare.com/ips-v6
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do
  firewall-cmd --permanent --zone=public \
    --add-rich-rule="rule family=ipv4 source address=$ip port port=443 protocol=tcp accept"
done
firewall-cmd --reload

Then remove the generic http/https service from the public zone. Use ufw or csf equivalents if that is your stack.

2. Restore the real client IP in nginx

Without this, nginx rate limits and logs see Cloudflare’s IPs, not the bot’s:

# /etc/nginx/conf.d/cloudflare-realip.conf
# Add one set_real_ip_from line per range from cloudflare.com/ips-v4 and ips-v6
set_real_ip_from 173.245.48.0/20;
# ...remaining Cloudflare ranges...
real_ip_header CF-Connecting-IP;

3. Managed Challenge the offending ASNs, not the whole site

In the Cloudflare WAF, create a custom rule scoped to the cloud networks showing up in your logs, and exclude verified bots so Googlebot and Bingbot are never challenged:

(ip.src.asnum in {132203 14061} and not cf.client.bot)

Action: Managed Challenge.

Replace the ASN list with what your own logs show. AS132203 (Tencent) and AS14061 (DigitalOcean) are examples backed by the reports above, not a universal list. Cloudflare recommends Managed Challenge over the other challenge types for most rules, because most humans pass it without interacting.

Why ASN and not country: a country block also hits real Singapore readers and recruiters. The cloud ASNs are where the automation lives. Real people browse from residential and mobile ISPs.

4. Harden the endpoints bots actually want

Most automated WordPress traffic targets a predictable set of paths. Protect those specifically:

(http.request.uri.path eq "/wp-login.php") or
(http.request.uri.path eq "/xmlrpc.php")

Block xmlrpc.php outright if you do not use Jetpack or the mobile app. Managed Challenge wp-login.php. Leave admin-ajax.php and /wp-json/ alone at the edge, for the reasons above.

5. Use the one free rate limit rule on the login page

On the Free plan, Cloudflare gives you one rate limiting rule, counting by IP, with a fixed 10 second period and 10 second mitigation timeout. That is too short for page traffic but fine for slowing login brute force. Point it at /wp-login.php.

Cloudflare notes that rate limit counters can lag by a few seconds, so some excess requests still reach the origin. That is why the origin needs its own limit.

6. Rate limit at nginx as the backstop

# http {} block
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=php:10m rate=10r/s;

# server {} block
location = /wp-login.php {
    limit_req zone=wplogin burst=3 nodelay;
    # ...your existing PHP-FPM handling...
}
location ~ \.php$ {
    limit_req zone=php burst=20 nodelay;
    # ...your existing PHP-FPM handling...
}

This only works correctly once step 2 is in place.

7. Make bot hits cheap

A bot that hits a cached page costs you almost nothing. A bot that hits uncached PHP costs you a worker and a database connection. Full page caching (at nginx or Cloudflare) turns a scraper flood from an outage into a bandwidth line item. Scrapers pulling random URLs that 404 still go through WordPress, so cache 404s briefly too if your stack supports it.

8. Filter GA4 separately

For the ghost hits, exclude the affected city and country combinations in GA4 reporting, or compare against server-side analytics. Do not change your firewall based on GA4 numbers alone. Your server logs are the source of truth for what reached the site.

Summary

Approach Stops origin load Hurts real users Breaks AJAX / webhooks Fixes GA4 ghosts
Under Attack mode (site-wide) Yes Yes, every visitor Yes No
Country block Partly Yes, real Singapore users No No
ASN Managed Challenge + endpoint rules + origin lock Yes Rarely No, if scoped No
GA4 filters No No No Yes

Under Attack mode is for the day you are actively being knocked offline. For a persistent scraper wave from a known set of cloud networks, scope the challenge to those networks, lock the origin, cache aggressively, and treat analytics noise as a separate problem.

Sources