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.
General rules
Section titled “General rules”- 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.
Base wrapper
Section titled “Base wrapper”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, )Node.js (Express)
Section titled “Node.js (Express)”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 windowconst LIMIT = 300; // request threshold from one client per windowconst 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);Python (FastAPI)
Section titled “Python (FastAPI)”The same on FastAPI: middleware with a sliding window and a “client share”:
import timefrom collections import defaultdict, dequefrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponsefrom notifly import notify
app = FastAPI()
WINDOW = 60 # window in secondsLIMIT = 300 # request threshold from one clientNOTIFY_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)What else to send
Section titled “What else to send”- 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.
Benefits
Section titled “Benefits”- 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.