Missing rate limiting: the hidden cost for side projects
Par Seaworthy · 16 avril 2026
Photo by Taylor Vick on Unsplash
The endpoint nobody thinks about until it costs them money
Side projects have a specific failure mode that big teams rarely fall into: you build the happy path, ship it, and then forget to ask what happens when someone hits your /api/send-email endpoint 10,000 times in an hour.
The answer is usually: your bill spikes, your service goes down, or both.
Rate limiting is one of those things that feels like over-engineering until the moment it isn't. You've got a contact form, a free-tier API, maybe a public endpoint that does something vaguely useful. None of them seem like targets. That's exactly the problem.
What actually happens without rate limiting
It's not just malicious actors. Misconfigured clients, retry loops gone wrong, scrapers, bots probing for open endpoints, a Reddit hug of death, a GitHub workflow that starts hammering your API because someone merged a broken environment variable. These are all just traffic. Without rate limiting, your app treats all of it the same as legitimate requests.
The practical outcomes:
- Your transactional email provider rate-limits you, and now real users can't get password reset emails
- Your LLM API bill doubles overnight because someone found a public inference endpoint
- Your database connection pool exhausts and the whole app goes unresponsive
- Your free-tier hosting provider suspends your account
The last one is genuinely the most annoying. You come back from a weekend to find your project is suspended and you have to explain yourself to a support bot.
The fix is not complicated
For most Node.js APIs, you can add basic rate limiting in about 20 lines of code. Here's the pattern using express-rate-limit:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per window
standardHeaders: true, // return rate limit info in RateLimit-* headers
legacyHeaders: false,
});
// Apply to all routes
app.use(limiter);
// Tighter limit on your most expensive endpoint
const heavyLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
});
app.use('/api/process', heavyLimiter);
That's it. It's not a production-grade solution for a high-traffic service, but for a side project it means you've stopped the easy abuse vectors. If you're on Cloudflare, their free plan includes rate limiting rules you can set up without touching your code at all.
For anything beyond a single server instance, you'll want Redis-backed rate limiting so the counter is shared across processes. The rate-limiter-flexible package handles this well, and if you're using Upstash you get Redis without managing infrastructure.
import { RateLimiterRedis } from 'rate-limiter-flexible';
import { Redis } from '@upstash/redis';
const client = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
const rateLimiter = new RateLimiterRedis({
storeClient: client,
keyPrefix: 'rl',
points: 10, // requests
duration: 60, // per second
});
One thing to be aware of with in-memory rate limiting: the counters live in the process. If you restart your app, the counters reset. That's usually fine for side projects, but it means a sustained attack can reset your window by triggering a crash. Not a reason to avoid it, just worth knowing.
The endpoints that need it most
Not every route carries the same risk. The ones that matter most for side projects:
Any endpoint that sends something out. Email, SMS, webhooks: these have real costs and third-party rate limits of their own. One abuse incident can burn through a month's free-tier allocation in minutes.
Anything that touches an LLM. Inference is expensive. If you're proxying calls to Claude or GPT and someone finds the endpoint, you're on the hook for the spend. Even a modest probe script can generate a meaningful bill.
Authentication endpoints. Login, signup, password reset: without rate limiting these are open to credential stuffing and enumeration attacks. A 429 response after 5 failed login attempts is table stakes.
Public search or data endpoints. If your endpoint does non-trivial computation or hits a database with an expensive query, it's worth protecting even if the data itself isn't sensitive.
What you're actually protecting against
I want to be specific here because "rate limiting prevents abuse" is vague enough to be useless. You're protecting against:
- Automated scraping that ignores
robots.txt - Credential stuffing (trying username/password pairs from leaked databases)
- API key enumeration (probing to find valid keys)
- Accidental self-DoS from your own broken retry logic
- Cost amplification attacks on proxied services
You're not protecting against a determined attacker with a botnet who can distribute requests across thousands of IPs. That's a different problem, and honestly not the one side projects face. The real threat is the opportunistic, low-effort abuse that bots run continuously across the internet looking for unprotected endpoints.
Basic rate limiting stops most of it.
The monitoring half of the problem
Rate limiting alone doesn't tell you when you're being targeted. Adding a counter metric or a simple log line when the limiter fires means you'll notice patterns: a spike of 429s from a single IP, a specific endpoint getting hammered, a bot that backs off and comes back every 16 minutes.
app.use((req, res, next) => {
res.on('finish', () => {
if (res.statusCode === 429) {
console.warn(`Rate limited: ${req.ip} ${req.method} ${req.path}`);
}
});
next();
});
It's not Datadog, but it's enough to know something is happening before it becomes a problem.
Seaworthy flags missing rate limiting as a resilience issue during static analysis, including common patterns like unprotected authentication endpoints and public API routes with no throttling in place.
Cet article a été généré par IA et résume des sources publiquement disponibles.
Seaworthy analyse votre dépôt pour détecter les problèmes abordés dans des articles comme celui-ci.
Failles de sécurité, secrets exposés et mauvaises configurations détectés avant le déploiement. Gratuit, aucun compte requis.