Menu

ESI Rate limiting - retrospective

Personal notes on a year of rate limits on ESI, and what we learned along the way.

By Xaroth Brook

It has been a little over a year since we announced rate limiting on ESI. The first group, /status, went live on 13 October 2025, and the rest followed in phases until early December. Market orders joined in February. That feels like enough time to look back on it.

This isn’t an official post. These are my own notes on why we did it, what we learned, and what it has changed for us.

Why rate limits at all

Before rate limits, we had one real tool against an application that was hurting ESI: a ban. A script would get stuck in a retry loop, or a scheduled job would start running a hundred times instead of once, and the load would land on ESI and on the game servers behind it. We would notice, track down the application and block it. Then we would try to reach the developer, who usually had no idea anything was wrong until their tool stopped working.

That wasn’t fun for anyone. The developer lost access with little warning, and we spent our time chasing traffic instead of building things. Almost none of those applications were doing anything malicious. They had a bug, or they were built on assumptions that used to be true.

So what we wanted was fairly simple:

  • Stop a runaway application before it hurts ESI or the systems behind it, without anyone on our side having to step in.
  • Write down what we consider acceptable and what we consider too much. Until then, that line only existed in our heads and on our dashboards.
  • Use the same rules for everyone. The biggest tools and the smallest scripts get the same buckets.
  • Stop needing to ban people.

That last one is the one I care about most, and it’s also the one that worked best. We used to ban several applications every week. These days it’s one or two a month, if that.

How it works

Every route belongs to a rate limit group, and every combination of group, application and character gets its own bucket of tokens. What a request costs depends on the response:

Response Cost
2XX 2 tokens
3XX 1 token
4XX 5 tokens
5XX 0 tokens

The costs say something about what we’d like to see. A 304 Not Modified is cheap for us, so it’s cheap for you too. Errors cost more, because a stream of 4XX responses is usually the first sign of an application stuck in a loop. And if we return a 5XX, that’s on us, so it costs nothing.

Tokens come back after 15 minutes. When a bucket runs dry you get a 429 with a Retry-After header that tells you how long to wait.

Explaining the window

This is the part that needed the most explaining. We often pointed people to smudge.ai’s post on rate limit algorithms, which has great interactive examples. We called ours a sliding window, but our implementation is closer to what Upstash and Cloudflare use: a floating window, sometimes called an approximated sliding window.

A true sliding window remembers when every request happened and counts the ones from the last 15 minutes. That’s accurate, but keeping a timestamp for every request in every bucket gets expensive at ESI’s volume. A floating window only keeps two numbers per bucket: tokens used in the current fixed window, and tokens used in the previous one. To estimate what you used in the last 15 minutes, it takes the current count and adds part of the previous count. How big that part is depends on how much of the previous window still falls inside those 15 minutes.

Written out, it looks like this:

used = previous x overlap + current

Cloudflare wrote a good explanation of the same idea. When they checked it against 400 million requests, only 0.003% of them were wrongly allowed or limited. Close enough, for a lot less memory.

Here’s a small version to play with. The limit is 6 hits and a window is 8 seconds, so you don’t have to wait 15 minutes to see what happens. Press Hit a few times in a row to run out.

What makes this hard to reason about is that tokens don’t all come back at a set time, like “5000 requests per hour, reset on the hour”. They trickle back as the previous window slides out of view. Most developers hadn’t run into this kind of limit before, so the documentation had to carry more of the load than it would for a fixed window. Our first version of it didn’t carry quite enough, and we’ve been adding to it since.

Limit, not target

Something I didn’t see coming was how people read the numbers. We meant the limit as a safety net. A lot of people read it as a target.

I get why. On the road, the speed limit is the speed you’re allowed to drive, and nobody gets pulled over for driving it. So if a group gives you a certain number of tokens per 15 minutes, why not use all of them, every 15 minutes?

From our side, the limit is the point where we’re fairly sure something has gone wrong. Most applications use a small part of it, which is normal and fine. Hitting it now and then isn’t a problem either. That’s what it’s there for.

History made this harder. A few routes already had limits before this system existed, and they had them because those routes were causing trouble further down the chain. People were used to going faster than that, so those limits felt like something being taken away. On top of that, some developers stayed within the published limits and still got banned, because their traffic was causing too much load anyway. From the outside, that looks like the rules changing under your feet, and I understand why it felt unfair.

Breaking that cycle was a big part of why we built this. The limits now do the job a ban used to do, only earlier, automatically, and with a clear message about what happened and how long to wait.

Wanting more data is fine

Wanting fresher data, sooner, is a perfectly good thing to want. Traders want the latest orders. Corporations want to know what’s in their hangars now, not an hour ago. Nobody builds a tool to make our lives harder.

But ESI sits in front of the game, and the game has its own limits. Every request that isn’t served from cache ends up somewhere that also has to keep New Eden running for everyone who’s logged in. Rate limits are how we make sure one bug in one tool can’t make that worse for everyone else.

Looking back

If I did it again, I’d spend more time on the explanation up front. A limit that’s written down and a limit that’s understood aren’t the same thing. I’d start with why we have limits, show something like the demo above, and only then get to the numbers. And I’d say much earlier and more plainly that the limit isn’t a target.

All in all, I’m happy with where it ended up. Far fewer bans, fewer surprises for developers, and one set of rules for everyone.