Skip to content

Anomalous traffic and API abuse notifications

While the project is small, there’s usually no proper abuse protection. Yet a single client hammering your endpoint means a traffic bill, a downed service, and potential scraping. We add lightweight middleware: count requests per IP or API key and send a push when someone spikes sharply or a wave of 429s begins.

  • Server-side only. Count on the backend; call notify() from there.
  • Throttle the pushes themselves. During an attack, don’t turn Notifly into the same storm — one push per client every N minutes.
  • Distinguish a spike from a launch. A sharp traffic increase may not be abuse but your own marketing launch. Look at concentration: one IP/key accounting for a large share of requests is abuse; traffic spread evenly across many addresses is more likely a rush of real people.

We take notify() from the overview page:

import os, requests
def notify(title, message, priority=5):
requests.post(
f"{os.environ['NOTIFLY_URL']}/message",
params={"token": os.environ["NOTIFLY_TOKEN"]},
json={"title": title, "message": message, "priority": priority},
timeout=5,
)

A sliding window per IP; a push when one client blows past the limit:

const {notify} = require('./notifly');
const WINDOW_MS = 60_000; // 1-minute window
const LIMIT = 300; // request threshold from one client per window
const NOTIFY_COOLDOWN = 300_000;
const hits = new Map(); // ip -> [timestamps]
const lastNotify = new Map(); // ip -> ts
function rateGuard(req, res, next) {
const ip = req.ip;
const now = Date.now();
const arr = (hits.get(ip) || []).filter(t => now - t < WINDOW_MS);
arr.push(now);
hits.set(ip, arr);
if (arr.length > LIMIT) {
// tell a spike from a general rush: this IP's share of all traffic
const total = [...hits.values()].reduce((s, a) => s + a.length, 0);
const share = arr.length / total;
if (share > 0.5 && now - (lastNotify.get(ip) || 0) > NOTIFY_COOLDOWN) {
lastNotify.set(ip, now);
notify(
'🚦 Аномальный трафик',
`IP ${ip}: ${arr.length} req/min (${(share * 100).toFixed(0)}% всего трафика)\n` +
`Путь: ${req.method} ${req.path}`,
8
);
}
return res.status(429).json({error: 'rate limit'});
}
next();
}
app.use(rateGuard);

The same on FastAPI: middleware with a sliding window and a “client share”:

import time
from collections import defaultdict, deque
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from notifly import notify
app = FastAPI()
WINDOW = 60 # window in seconds
LIMIT = 300 # request threshold from one client
NOTIFY_COOLDOWN = 300
hits: dict[str, deque] = defaultdict(deque)
last_notify: dict[str, float] = defaultdict(float)
@app.middleware("http")
async def rate_guard(request: Request, call_next):
ip = request.client.host
now = time.time()
q = hits[ip]
q.append(now)
while q and now - q[0] > WINDOW:
q.popleft()
if len(q) > LIMIT:
total = sum(len(v) for v in hits.values())
share = len(q) / total if total else 0
# one client's spike, not a general rush
if share > 0.5 and now - last_notify[ip] > NOTIFY_COOLDOWN:
last_notify[ip] = now
notify(
"🚦 Аномальный трафик",
f"IP {ip}: {len(q)} req/min ({share*100:.0f}% всего трафика)\n"
f"Путь: {request.method} {request.url.path}",
8,
)
return JSONResponse({"error": "rate limit"}, status_code=429)
return await call_next(request)

Spike vs. launch: a quiet signal about a rush

Section titled “Spike vs. launch: a quiet signal about a rush”

If traffic grew but is spread across many addresses, it’s most likely good news (the ad worked). That’s worth knowing too, but quietly:

# a separate quiet check once a minute (e.g. from a background task)
def traffic_pulse(rpm: int, unique_ips: int):
if rpm > 1000 and unique_ips > 100:
# many requests from many people — this is a rush, not abuse
notify("📊 Наплыв трафика", f"{rpm} req/min от {unique_ips} IP — похоже, запуск зашёл", 4)
  • Many 401/403 in a row from one IP — password or token guessing; priority 8.
  • Listing scraping. A sequential sweep of ?page=1,2,3… faster than a human.
  • A 5xx surge during a rush — the service is struggling; priority 9.
  • The threshold and “health” are convenient to offload to the HTTP monitor.
  • You see an attack at the start, not from the end-of-month traffic bill.
  • Fewer false alarms. The client share separates a real spike from your own successful launch.
  • Throttling built-in — during an attack the phone doesn’t ring nonstop.