DDoS vs Legit Traffic Confusion — How to Tell a Viral Moment From an Attack

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

A sudden 100x traffic spike is simultaneously your best and worst scenario. Best: your product went viral. Worst: you are under attack. The problem is that the initial signals look identical — both show a dramatic request rate increase. The correct response diverges completely: viral traffic needs capacity, attack traffic needs blocking. Misidentifying either direction is expensive. This guide builds the measurement framework to distinguish them under pressure.

The Key Signals That Separate Attacks from Viral Traffic

You cannot rely on raw request volume. The distinguishing signals are in the structure and behavior of the traffic:

Signals pointing to DDoS / attack:
→ Single IP or ASN contributing > 10% of total request volume
→ Requests concentrated on one endpoint (not organic browsing behavior)
→ No session continuity: no cookies, no follow-up requests, no login flows
→ User-agent strings are uniform, absent, or follow a known bot pattern
→ Requests are identical or near-identical in payload and timing
→ Geographic concentration: 95%+ from one country or cloud/datacenter ASN
→ Traffic starts and stops abruptly with no ramp-up
 
Signals pointing to legitimate viral spike:
→ Traffic from thousands of unique IPs across diverse ASNs
→ Multiple endpoints hit in sequence: landing page, signup, product, FAQ
→ Session-like behavior: cookies, referrer chains from press/social
→ Varied user agents across Chrome, Safari, Firefox, mobile browsers
→ Realistic conversion rates — signups and purchases rising proportionally
→ Referrer origins match the viral source (HN, Reddit, TechCrunch)
→ Traffic has a natural ramp curve matching news/social publication time

Building a Real-Time Traffic Classifier

Pre-build this analysis tool. Running SQL analysis during an incident is too slow:

import psycopg2
from datetime import datetime, timedelta
 
def analyze_traffic_quality(window_minutes=5):
    cutoff = datetime.utcnow() - timedelta(minutes=window_minutes)
 
    conn = psycopg2.connect(dsn="...")
    cur = conn.cursor()
 
    # IP diversity ratio
    cur.execute("""
        SELECT
            COUNT(DISTINCT ip_address) AS unique_ips,
            COUNT(*) AS total_requests,
            COUNT(DISTINCT ip_address)::float / NULLIF(COUNT(*), 0) AS diversity_ratio
        FROM request_logs
        WHERE created_at > %s
    """, [cutoff])
    ip_stats = cur.fetchone()
 
    # Top IP concentration
    cur.execute("""
        SELECT ip_address,
               COUNT(*)::float / (
                   SELECT COUNT(*) FROM request_logs WHERE created_at > %s
               ) AS share
        FROM request_logs
        WHERE created_at > %s
        GROUP BY ip_address
        ORDER BY share DESC
        LIMIT 1
    """, [cutoff, cutoff])
    top_ip = cur.fetchone()
 
    # Endpoint concentration
    cur.execute("""
        SELECT path,
               COUNT(*)::float / (
                   SELECT COUNT(*) FROM request_logs WHERE created_at > %s
               ) AS share
        FROM request_logs
        WHERE created_at > %s
        GROUP BY path
        ORDER BY share DESC
        LIMIT 1
    """, [cutoff, cutoff])
    top_endpoint = cur.fetchone()
 
    # User-agent diversity
    cur.execute("""
        SELECT COUNT(DISTINCT user_agent) AS unique_uas
        FROM request_logs WHERE created_at > %s
    """, [cutoff])
    ua_diversity = cur.fetchone()[0]
 
    conn.close()
 
    top_ip_share = top_ip[1] if top_ip else 0
    top_endpoint_share = top_endpoint[1] if top_endpoint else 0
 
    score = 100
    if top_ip_share > 0.10:
        score -= 40   # single IP > 10% of traffic: strong attack signal
    if top_endpoint_share > 0.70:
        score -= 30   # > 70% on one endpoint: attack pattern
    if ua_diversity < 5:
        score -= 20   # fewer than 5 distinct user agents: bot traffic
    if ip_stats[2] < 0.01:
        score -= 10   # very low unique IP ratio
 
    return {
        "legitimacy_score": score,
        "total_requests": ip_stats[1],
        "unique_ips": ip_stats[0],
        "top_ip_share": round(top_ip_share, 3),
        "top_endpoint_share": round(top_endpoint_share, 3),
        "ua_diversity": ua_diversity,
        "verdict": "legitimate" if score > 60 else "attack",
        "action": "scale up" if score > 60 else "activate mitigation"
    }

Run this every 2 minutes during elevated traffic and log the output to your monitoring system.

Tiered Response: Rate Limit Before Blocking

Never jump straight to blocking. Blocking real users who look slightly bot-like is a business cost. Use progressive mitigation:

// Tiered mitigation based on signals
function getMitigationLevel(botScore, ipReputation) {
  if (botScore > 90 || ipReputation < 10) return 'block'
  if (botScore > 60 || ipReputation < 40) return 'challenge'
  if (botScore > 30 || ipReputation < 70) return 'rate_limit'
  return 'none'
}
 
async function applyMitigation(req, res, next) {
  const botScore = calculateBotScore(req)   // fingerprinting, timing, headers
  const ipRep = await getIPReputation(req.ip)  // AbuseIPDB or IPQualityScore
  const level = getMitigationLevel(botScore, ipRep)
 
  switch (level) {
    case 'block':
      return res.status(403).json({ error: 'Access denied' })
 
    case 'challenge':
      // JS challenge: real browsers pass, most bots fail
      if (!req.cookies['cf_clearance'] && !req.headers['x-challenge-passed']) {
        return res.status(429).send(generateChallengePage())
      }
      break
 
    case 'rate_limit':
      const allowed = await checkStrictRateLimit(req.ip)
      if (!allowed) return res.status(429).json({ error: 'Rate limit exceeded' })
      break
  }
 
  next()
}

Cloudflare Under Attack Mode via API

When a confirmed DDoS is underway, activate Cloudflare's JS challenge for all visitors automatically:

async function activateUnderAttackMode() {
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/zones/${process.env.CF_ZONE_ID}/settings/security_level`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.CF_API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ value: 'under_attack' })
    }
  )
 
  if (!response.ok) {
    throw new Error(`Cloudflare API error: ${response.status}`)
  }
 
  console.log('Under Attack mode activated — JS challenge for all visitors')
}
 
async function deactivateUnderAttackMode() {
  await fetch(
    `https://api.cloudflare.com/client/v4/zones/${process.env.CF_ZONE_ID}/settings/security_level`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.CF_API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ value: 'medium' })
    }
  )
  console.log('Under Attack mode deactivated')
}

Under Attack mode adds a 5-second JavaScript computation challenge. Real browsers complete it automatically. Scripted attacks cannot.

Protecting Origin IP During DDoS

DDoS attackers frequently discover the origin IP and bypass the CDN entirely. Protect your origin by accepting traffic only from Cloudflare IP ranges at the network level (firewall or nginx):

# nginx: only accept traffic from Cloudflare
real_ip_header CF-Connecting-IP;
 
# Allow Cloudflare IPv4 ranges (update from cloudflare.com/ips-v4)
allow 103.21.244.0/22;
allow 103.22.200.0/22;
allow 104.16.0.0/13;
allow 104.24.0.0/14;
allow 108.162.192.0/18;
allow 131.0.72.0/22;
allow 141.101.64.0/18;
allow 162.158.0.0/15;
allow 172.64.0.0/13;
allow 173.245.48.0/20;
allow 188.114.96.0/20;
allow 190.93.240.0/20;
allow 197.234.240.0/22;
allow 198.41.128.0/17;
deny all;

Incident Response Runbook

Encode the decision tree before an incident — reasoning during one is unreliable:

T+0 to T+5 min — Detection
  Run traffic classifier
  Check Cloudflare analytics for request volume and origin breakdown
  Check application error rate in APM
  Post to incident channel: "elevated traffic, investigating"
 
T+5 to T+10 min — Classification
  Legitimacy score > 60?  → viral traffic: scale capacity
  Legitimacy score < 60?  → suspected attack: continue below
  Check referrer origins — is there a press article, HN/Reddit post?
  Check top IP share — single source or distributed?
 
T+10 min — Mitigation (if attack confirmed)
  Level 1: Raise Cloudflare security level to "High"
  Level 2: Activate "Under Attack" mode (JS challenge all visitors)
  Level 3: Geo-block if attack is geographically concentrated
  Level 4: Engage DDoS protection vendor if subscribed
 
Recovery
  Re-run classifier every 5 minutes
  Deactivate mitigations when legitimacy score returns above 70
  Schedule post-mortem: what was the attack vector?

Key Takeaways

  • Raw request volume is not a useful signal — both viral spikes and DDoS attacks produce it; look at IP diversity, endpoint concentration, and user-agent distribution
  • A single IP contributing more than 10% of traffic is a strong attack signal; organic viral traffic rarely exceeds 1% from any single source
  • Endpoint concentration above 70% on one path (especially non-browsable paths) indicates targeted attack, not organic traffic
  • Tiered mitigation (rate limit → challenge → block) avoids the false positive cost of blocking legitimate users who fail simple heuristics
  • Build the traffic classifier and Cloudflare automation before an incident — live SQL analysis during a DDoS is too slow
  • Protect origin IP at the network layer by whitelisting only CDN IP ranges; attackers who discover origin IPs bypass all CDN-level protections
  • Store Cloudflare API credentials as environment variables and test activation quarterly so the runbook works under pressure
  • After mitigation, deactivate aggressively — "Under Attack" mode degrades legitimate user experience for real browsers too

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading