← All news
resilience5 min read

Missing rate limiting: the hidden cost for side projects

By Seaworthy · April 16, 2026

Dense tangle of network cables plugged into server rack ports, representing high traffic and server infrastructure

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.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode