← 全部新闻
resilience5 分钟阅读

What happens when your side project hits the front page with no rate limiting

作者:Seaworthy · 2026年4月6日

Laptop screen showing a performance analytics dashboard with graphs and traffic metrics

Photo by Luke Chesser on Unsplash

The front page is not the reward you think it is

You ship a side project on a Sunday afternoon. It's rough around the edges but it works. You post it to Hacker News more or less on a whim. Then you go to bed.

At 2am your phone buzzes. Someone is DMing you to say your site is down. You check, and yeah, it's returning 503s to everyone. Meanwhile your post is sitting at number four on the front page and getting hammered. You just got exactly what every indie developer says they want, and your server is on its knees.

This is not hypothetical. It happens constantly, and the fix is almost always the same: no rate limiting anywhere, a database connection pool that's been exhausted, and a server that was sized for "a handful of people trying it" rather than "three thousand concurrent users for two hours."

Let me walk through what actually breaks, in roughly the order it breaks.


First: your database connections

Most web frameworks open a database connection per request, or grab one from a pool. If you're using something like Postgres with a default max: 10 pool size and you suddenly get 500 concurrent requests, those requests queue up waiting for a connection. The queue fills up. New requests get rejected or time out. Your server logs fill with ECONNRESET and too many connections errors.

The fix is boring but real. Set a sensible pool size and add a connection timeout:

// Postgres via pg or Knex
const pool = new Pool({
  max: 20,
  connectionTimeoutMillis: 2000,
  idleTimeoutMillis: 30000,
});

That connectionTimeoutMillis matters more than people realise. Without it, requests just hang forever waiting for a connection that will never come. With it, you fail fast and your server stays responsive even when overloaded.


Second: no rate limiting means one person can take you down

Without rate limiting, a single automated client hitting your /api/search endpoint a hundred times a second will consume all your server's capacity. This isn't necessarily malicious. It could be someone's script, a misconfigured bot, or a user who double-clicked something and triggered a retry loop.

The absolute minimum is a per-IP request limit. In Express, this is three lines:

import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 60,             // 60 requests per IP per minute
  standardHeaders: true,
  legacyHeaders: false,
});

app.use("/api", limiter);

That's it. That one block would have saved a lot of people a lot of grief.

If you have endpoints that are more expensive (anything that hits an LLM, sends an email, or does a full-text search), give them their own stricter limit. A 10 requests per minute limit on /api/generate and a 60 per minute limit on /api/search is not over-engineering. It's just treating your server like a finite resource.


Third: what actually happens at the OS level

When your Node process is saturated, it doesn't crash immediately. First the event loop lag climbs. Requests that used to take 50ms start taking 800ms. Users see a slow site and hit refresh, doubling the load. Eventually the process runs out of memory or file descriptors, and it crashes. If you don't have a process manager configured, it stays down.

Run your app under something that will restart it:

# If you're on a plain VPS
pm2 start app.js --name myapp
pm2 startup   # persist across reboots
pm2 save

This doesn't fix the overload problem but it means your server at least comes back up after a crash rather than sitting there dead until you wake up.


The things that feel optional until they aren't

Health check endpoints. If you're behind any kind of load balancer or reverse proxy, you need a /health route that returns 200 when the server is up. Without it, you can't tell from the outside whether the process is alive.

Request timeouts. If a downstream service (your database, an external API) hangs, your request will hang too, holding a connection open. Set a global timeout:

app.use((req, res, next) => {
  res.setTimeout(10000, () => {
    res.status(503).json({ error: "Request timeout" });
  });
  next();
});

Structured logging. When you're debugging a 2am outage, console.log("something went wrong") is useless. Even switching to JSON logs with a timestamp and request ID makes the difference between finding the problem in five minutes and guessing for two hours.


Why this keeps happening

I think the honest answer is that rate limiting and connection pool tuning feel like premature optimisation when you're building something nobody uses yet. And most of the time they are. Your side project will probably never get enough traffic to matter.

But "probably never" is doing a lot of work in that sentence. The cost of adding a rate limiter is about ten minutes. The cost of not having one when your post hits the front page is your server being down for the best two hours of traffic you'll ever get.

The configuration stuff, the rate limits, the timeouts, the process manager: none of it is glamorous. It's the kind of thing that's easy to skip when you're in the "just get it working" phase and never go back to.

Seaworthy's ops basics and resilience checks flag missing rate limiting, absent health check endpoints, and misconfigured connection handling in your codebase before you need to care about them.

本文由 AI 生成,总结了公开可用的来源。

来自 Seaworthy 团队

Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。

安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。

$ npx seaworthycode