← ALL FIELD NOTES

LOG 054 · TECHNICAL · 2025-04-10

Built a global IP rate limiter in a day to stop bot scraping

1 min read

Situation

Goodpods is a podcast app, and I worked on its API. In April 2025 the API came under aggressive bot scraping. There was no abuse-protection layer at that point, so we needed one fast.

Task

I took on building a global rate limiter for the whole API and getting it live the same day. The design, the middleware, and the rollout were mine.

Action

I wrote a new Starlette middleware that applied a per-IP request limit to every endpoint, backed by Redis. I made the Redis connection initialise lazily so the middleware could never block app startup.

Not all traffic should be limited the same way. I allowlisted our frontend server’s IP and added a bypass for authenticated Next.js server-side rendering, where our own frontend server makes API calls on behalf of users, so one server IP legitimately carries many people’s traffic.

For the threshold we had no historical data, so we measured instead of guessing blind. We went into the app and clicked around like mad for five minutes, then extrapolated. That gave 5,000 requests per hour, which should be enough for any human. Real traffic later proved that too aggressive, so I widened the limit to 50,000 per hour.

The first version read the client IP from request.client.host. Behind GCP’s load balancer that is the balancer’s own IP, which would have counted every user as a single client and throttled the whole site together. We caught it in development before it went live, and I fixed it the same day by reading X-Forwarded-For, with safe handling for a missing or malformed header.

Result

The rate limiter put out the fire quickly and was effective at reducing bot traffic for several months. The load balancer IP bug never reached production because we tested against the real deployment setup first.