The Five Waves: How a DDoS Attack Nearly Took Down India's Largest Cancer Care Platform

CloudflareDevOpsIncident ResponseSecurity
The Five Waves: How a DDoS Attack Nearly Took Down India's Largest Cancer Care Platform

The Five Waves

How we survived a DDoS siege on India's largest cancer and critical-care medicine platform — and what it taught me about building things that can't afford to go down


12:07 AM, May 16th

The dashboard is a wall of red.

Four days in, and I've stopped flinching at the numbers — I've started reading them like a heartbeat monitor. Request count climbing. Origin latency climbing. A single product page, Erbitux 500mg injection, is being hit so hard it looks less like traffic and more like an assault. Which, by this point, we know it is.

Somewhere on the other end of this, a patient's caregiver is trying to check if this chemotherapy drug is in stock. They can't. Because somewhere else entirely, a botnet doesn't care.

This is the story of how that happened — and the five-day scramble to stop it, told the way I actually lived it: not as a clean timeline, but as a slow, sickening realization that the front door we thought was locked had never really been closed at all.

Then, after the story, the part I wish someone had handed me on Day 1: the actual playbook — DNS hygiene, WAF configuration, edge fingerprinting, cache strategy, the whole thing — so if you're reading this at 12:07 AM with your own dashboard on fire, you don't have to learn it the hard way.


Act I — The First Tremor

May 15, 2026.

It starts small. A blip. Traffic to one product page — a high-value oncology drug, the kind of page that should see a few hundred hits a day from legitimate buyers and hospital procurement teams — spikes without warning.

Nobody panics. Traffic spikes happen. Maybe a hospital sent out a bulk inquiry link. Maybe it's a crawler having a bad day. We watch it, half-convinced it'll flatten out on its own.

It doesn't. It comes back the next day. Bigger.

That's the thing about the early stage of an attack like this — it doesn't announce itself. It looks exactly like the thing you'd want: interest in your platform. By the time you realize it isn't organic, it's already Wave Two.

Act II — Five Waves

Over the next four days, the attack returns five separate times, each wave larger and more targeted than the last, all converging on the same page: the Erbitux listing. By the time it's over, we're looking at roughly 13.5 million requests hitting Vercel directly, and over 27 million requests at the Cloudflare edge.

The choice of target wasn't random, and that's part of what made it unsettling. It wasn't the homepage. It wasn't a generic flood across the whole domain. It was a single, specific, high-traffic medical page — the kind of surgical targeting that suggested this wasn't a script kiddie testing a botnet, it was someone (or something) that had actually looked at the site first.

Every wave followed a similar shape: a sudden ramp-up in requests, origin servers straining under load they were never provisioned for, and — critically — nothing stopping it at the edge. Which made no sense. We had Cloudflare sitting in front of the site. Cloudflare is supposed to be the wall. Why was the wall letting everything through?

Act III — The Twist

This is the part of the story that still makes my stomach drop a little, because it's the kind of mistake that's invisible until it's catastrophic.

We went digging into the DNS configuration expecting to find a misconfigured WAF rule, or a rate limit set too generously. Instead we found something worse: a handful of the DNS records for the attacked subdomain were set to DNS-only — "grey-clouded" — instead of proxied through Cloudflare.

In Cloudflare, that toggle between the little grey cloud and the little orange cloud is the entire difference between "Cloudflare protects this traffic" and "Cloudflare has literally never seen this traffic." A grey-clouded record resolves straight to the origin IP. No WAF. No bot-fight mode. No rate limiting. No caching. The attacker wasn't breaking through our defenses — there were no defenses in that specific path to break through. Every request went straight to the same servers holding up the rest of the platform.

That's the Nolan-movie beat of this whole story: the vulnerability wasn't a zero-day, wasn't some exotic exploit. It was a checkbox. One flipped switch, sitting quietly in a DNS table, waiting for someone to find it.

Act IV — The Counter-Offensive

Once we knew what we were looking at, the response moved fast, on two fronts simultaneously.

Sealing the wall. We flipped every relevant record to orange-cloud, proxying all traffic through Cloudflare properly. Then we layered defenses on top: Cloudflare WAF custom rules tuned to the attack pattern, Super Bot Fight Mode engaged, and dedicated cache rules carved out for the highest-value, highest-risk paths — /medicines/, /otc/, and /_next/image — so that even a flood of requests for the same product page would mostly get served from cache instead of hammering origin.

Fingerprinting at the edge. WAF rules based on IP and request rate weren't going to be enough against a botnet that could rotate IPs. So we went a layer deeper and added JA4-based firewall rules at Vercel — fingerprinting the TLS/client handshake characteristics of the attacking traffic rather than just its source address, which is much harder for an attacker to spoof or rotate around.

The nuclear option. At the peak of the chaos, close to midnight, we made the call to migrate the entire deployment to a new Vercel account — a full account-level reset to shed whatever load and lingering exposure had accumulated, and buy ourselves clean footing to apply the new protections without fighting an active fire at the same time.

By the early hours, the fifth wave broke against a wall that actually existed this time. It didn't get through.

Act V — The Aftermath Nobody Warns You About

Here's the part of DDoS response that doesn't make it into most war stories: fixing the attack breaks other things.

Once Cloudflare's bot protection was cranked up to survive the siege, it started catching things we needed to let through. The Next.js server-side image optimizer — which fetches images server-to-server — started getting blocked as if it were the attacker, and product images across the site quietly stopped loading. Separately, Cloudflare's managed robots.txt had overwritten our origin's actual robots file, and Googlebot was being blocked from crawling the sitemap — a slow-bleed SEO wound we wouldn't have found if we hadn't gone looking.

Neither of these were dramatic. Neither would have made a dashboard turn red. But both would have quietly cost the business real money and real visibility for weeks if we hadn't specifically audited for collateral damage after the fire was out. That's the lesson buried in Act V: the attack ends, but the incident doesn't, until you've checked what your own defenses broke.

Epilogue

I came out of that week with something I didn't have going in: a genuine, felt-in-the-bones understanding that "we have Cloudflare in front of the site" and "Cloudflare is actually protecting the site" are two completely different claims, and the gap between them is invisible until someone exploits it.

It reshaped how I think about infrastructure as a founding engineer. Not as a checklist you complete once, but as a set of claims you have to keep re-verifying — because the failure mode isn't always an attacker outsmarting your defenses. Sometimes it's just a stale setting nobody rechecked since the DNS was first set up.


The Playbook: A DDoS Prevention & Response Guide (Technical Deep-Dive)

This section assumes a stack like ours — Cloudflare in front, Vercel (or similar) as host/origin, Next.js application layer — but the concepts port to any CDN/WAF + origin setup. Every rule below is written close to the actual syntax you'd type in, not just the concept, so you can copy-adapt it directly.

If you're mid-incident right now, jump to Section 5 (Incident Runbook) first.

1. DNS & Proxy Audit (root-cause layer — check this first, not last)

This is the layer that actually failed for us. Everything downstream is irrelevant if this is wrong.

Audit via API, not the dashboard — the dashboard makes it easy to miss one record in a long list. Pull the full zone and grep for anything not proxied:

curl -s -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records?per_page=500" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
| jq '.result[] | select(.proxied == false) | {name, type, content}'

Any A/AAAA/CNAME record in that output serving live user traffic is a record with zero WAF, zero rate limiting, zero bot management, zero caching in front of it — requests resolve straight to origin IP. Treat every hit in that list as a P0 until reviewed.

  • Enable SSL/TLS → Full (Strict), not just Full — Strict validates the origin cert, closing off origin-spoofing paths.
  • Enable HSTS with includeSubDomains so a downgrade attack can't route traffic around the proxy via plain HTTP.
  • Hide and rotate your origin IP if it's ever been exposed (old DNS history, leaked in headers, exposed via direct-IP hosting docs). If an attacker has your real origin IP, DNS-layer protection at Cloudflare is moot — they hit the IP directly. Use firewall rules at the origin (see Section 3) to only accept traffic from Cloudflare's published IP ranges.

2. Rate Limiting (the layer most teams under-configure)

Generic Cloudflare "Rate Limiting Rules" (not legacy Rate Limiting) let you scope by URI, method, and a counting key. For a hot product/detail page pattern like ours:

Rule name: rl-product-detail-pages
When incoming requests match:
  (http.request.uri.path matches "^/medicines/.*$") or
  (http.request.uri.path matches "^/otc/.*$")

Rate: 60 requests / 60 seconds
Counting characteristic: IP address (or, better, cf.bot_management.ja4 if on Enterprise)
Action: Managed Challenge (not Block, for the first tier — see below)

Two things matter more than the raw threshold:

  • Layer your rate limits by severity, don't use one blanket rule. Example ladder:
  • Count by more than just IP. IP-only counting is trivially defeated by a distributed botnet — each bot node individually stays under threshold. Where available, count by cf.bot_management.ja4 (client fingerprint) or a composite key (IP + User-Agent + cf.bot_management.ja3_hash) so distributed-but-similar clients still trip the rule.

At the Vercel/hosting layer, pair this with Vercel's Firewall rate limiting (Attack Challenge Mode) as a second independent layer — if Cloudflare is ever bypassed (as ours was via the grey-cloud gap), you want a rate limit that doesn't depend on Cloudflare being in the path at all.

3. WAF Rules & Origin Lockdown

Custom WAF expression to catch the shape of the attack we saw — high-frequency requests to a narrow path set, low variety in query strings, often missing normal browser headers:

(http.request.uri.path matches "^/(medicines|otc)/.*$") and
(not http.request.headers["accept-language"][0] contains "en") and
(cf.bot_management.score lt 30)

cf.bot_management.score (Cloudflare Bot Management / Super Bot Fight Mode) gives a 1–99 likelihood-of-bot score — tune the threshold against your own traffic logs before an attack, not during one, since legitimate API integrations and server-to-server calls can score low too (this is exactly what bit us with the Next.js image optimizer — see Section 6).

Lock the origin down to Cloudflare-only traffic so a leaked or historical origin IP can't be hit directly, bypassing Cloudflare entirely:

# Example: iptables allow-list restricted to Cloudflare's published IPv4 ranges
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do
  iptables -A INPUT -p tcp -s $ip --dport 443 -j ACCEPT
done
iptables -A INPUT -p tcp --dport 443 -j DROP

On Vercel specifically, use Authenticated Origin Pulls (Cloudflare presents a client cert Vercel can verify) so origin only accepts requests that actually came through the Cloudflare edge.

4. Fingerprinting Beyond IP (JA3/JA4)

IP-based defenses degrade fast against a real botnet — this is the layer that actually held for us after the DNS fix.

  • JA4/JA3 fingerprints the TLS Client Hello (cipher suites, extensions, ordering) — two requests from different IPs but the same attack tooling produce the same JA4 hash, even as source IPs rotate.
  • At Vercel, this is configured as a Firewall rule keyed on the JA4 fingerprint rather than IP:
Rule: block-known-attack-ja4
Condition: request.ja4 == "t13d1516h2_8daaf6152771_02713d6af862"  // example hash, replace with observed value from your attack logs
Action: Deny

Get the actual hash from your attack traffic first — pull it from Cloudflare's HTTP request logs (cf.bot_management.ja4 field in Logpush) or Vercel's firewall logs during the live incident, then convert it into a standing block rule for the post-incident hardening pass.

  • Don't rely on User-Agent strings for anything — they're the single easiest header to spoof and carry near-zero signal in an actual DDoS.

5. Incident Runbook (do this in order, during a live attack)

  1. Verify the traffic is actually reaching your proxy layer, not bypassing it. Pull real-time analytics from Cloudflare (GraphQL Analytics API or dashboard) for the affected path. If Cloudflare's request count is much lower than your origin/Vercel request count for the same window, you have a bypass — go straight to the DNS audit in Section 1, don't waste time tuning WAF rules that traffic never passes through.
query {
  viewer {
    zones(filter: { zoneTag: "your_zone_id" }) {
      httpRequests1mGroups(
        limit: 60
        filter: { datetime_geq: "2026-05-19T00:00:00Z" }
      ) {
        dimensions { datetime }
        sum { requests }
      }
    }
  }
}
  1. Enable "Under Attack Mode" at the zone level (or affected subdomain) — this forces a JS/Turnstile challenge on every request before it reaches origin. It costs you some legitimate-user friction; it's worth it to stop the bleeding while you build targeted rules.
  2. Deploy a scoped Managed Challenge rule on the specific attacked path(s) immediately (Section 2's Tier 1 rule) rather than waiting to build a perfectly-tuned rule — a rough rule live now beats a precise rule live in 40 minutes.
  3. Pull the JA4/IP/ASN pattern from live logs, feed it into a targeted Block rule (Section 4), then relax Under Attack Mode back to normal once the targeted rule is confirmed effective — Under Attack Mode isn't sustainable for a multi-day siege, it degrades UX for real customers.
  4. Have a clean-slate fallback documented in advance: for a hosting-account-level migration (our "nuclear option"), you need CI/CD, environment variables, and DNS cutover steps written down before the incident. Mid-incident is the worst time to be reconstructing a deploy pipeline from memory.

6. Post-Incident: Verify You Didn't Break Legitimate Traffic

Tightened bot rules and Under Attack Mode create false positives. Check specifically for:

  • Server-to-server calls scoring as bots. Next.js's server-side image optimizer, internal health checks, webhook receivers, and partner API calls often present with none of the "normal browser" signals (no cookies, no JS execution, sparse headers) — the same profile as an attack bot. Add explicit WAF Skip rules or IP allowlist entries for known-good server-to-server sources:
Rule: allowlist-internal-image-optimizer
When: (ip.src in {your_known_server_ips}) 
Then: Skip → Bot Fight Mode, Rate Limiting, WAF Managed Rules
  • Crawler access. Confirm robots.txt being served is actually your origin's file, not a Cloudflare-managed default that silently overwrote it — curl -I https://yourdomain.com/robots.txt and diff against what's in your repo. Then confirm via Google Search Console that Googlebot's crawl requests aren't showing 403s in the same window as your attack response.
  • Cache correctness. Verify Cache-Control and edge TTLs on the hardened paths didn't get set so aggressively that legitimate stock/price updates are now serving stale data — check stale-while-revalidate windows against how frequently that data actually changes.

7. Monitoring to Build Before You Need It

  • Alert on request-rate deviation, not just absolute volume — a page that normally does 200 req/day hitting 5,000 in an hour matters more than your homepage's raw traffic doubling on a marketing push. Baseline is per-path, not per-zone.
  • Alert on the ratio of edge requests to origin requests dropping — this is the single metric that would have caught our grey-cloud gap in minutes instead of days. If origin traffic isn't proportionally reflected at the edge, something is bypassing your proxy.
  • Log and retain cf.bot_management.score and JA4 hashes (via Cloudflare Logpush to your own storage) even during normal operation — during an attack, you want historical baseline to diff against, not just the live spike.

If there's one line I'd want someone to walk away with: a DDoS defense is not something you configure once and trust forever — it's a claim about your infrastructure that you have to keep re-verifying, because the gap between "we have protection" and "we have protection everywhere it needs to be" is exactly where attackers live. The checkbox that almost took us down wasn't a missing rule — it was a rule that was never actually in the request's path to begin with.