Bypassing Rate Limits Using The Host Header
How two domains opened the door
During a round of testing on an API, I noticed something that did not quite line up with what I expected from the rate limiting in place. Two separate domains, domain1[.]com and domain2[.]com, both pointed to the same backend. Even though they shared the same infrastructure, they seemed to track rate limits separately. By changing the Host header between the two, I was able to send more requests than the system should have allowed.
A quick note before going further. Since testing is ongoing for this client, all names and values here are replaced with stand-in data.
What I observed while testing
When I sent repeated requests to domain1[.]com/<user>/, the system eventually started blocking me after roughly one thousand requests. That part made sense. What surprised me was that, if I then sent the same type of requests but changed the Host header to domain2[.]com, the server allowed another large batch of requests without complaint, even though both domains relied on the same backend.
To confirm they actually shared the same backend, I ran a few simple DNS lookups and checked the records:
$ dig domain1.com +noall +answer
domain1.com. 0 IN CNAME cname1.com.
cname1.com. 0 IN A x.x.x.19
$ dig domain2.com +noall +answer
domain2.com. 0 IN CNAME cname1.com.
cname1.com. 0 IN A x.x.x.19
$ dig -x x.x.x.19 +noall +answer
x.x.x.31.in-addr.arpa. 0 IN PTR PTR1.com.
Both domain1[.]com and domain2[.]com resolved to the same IP address x.x.x.19. So while they looked like different entry points from the outside, behind the scenes they led to the same place.
Numbers that tell the story
To understand how much extra traffic this allowed, I recorded how many requests each domain would accept before the rate limit kicked in. The table below summarizes what I saw during testing:
| Endpoint | Host header | Requests before block |
|---|---|---|
| Node (API) | domain1.com | 1,138 |
| Node (API) | domain2.com | 1,098 |
| Query (GraphQL) | domain1.com | 3,570 |
| Query (GraphQL) | domain2.com | 3,808 |
By swapping the Host header between domain1[.]com and domain2[.]com, I could almost double the amount of traffic I was able to send before running into any limits. For someone who wants to push a system hard, this is more than enough to matter.
Why this behavior is concerning
- More effective scraping. If someone wants to scrape data, they can stretch out the limit simply by rotating through domains that share the same backend.
- Extra load on servers. Allowing more requests than expected can place additional stress on backend systems and introduce performance issues for real users.
- Gaps in detection. If monitoring and alerting rely heavily on rate limit triggers, this type of behavior might allow aggressive traffic to avoid drawing attention.
Some possible explanations
- Limits tracked per domain. The rate limiting logic may be using the value of the Host header as part of the key to track usage. If that is the case, each domain ends up with its own counter even though they share the same backend.
- Behavior at the load balancer or proxy. A load balancer or reverse proxy might be applying rate limits before traffic reaches the application. If limits are enforced per incoming domain rather than per shared backend, this behavior would make sense from the system's point of view.
- Separate contexts that do not talk to each other. It is also possible that the system maintains separate rate limit contexts for each domain for operational reasons, but did not fully account for the fact that some clients may be able to move between those domains easily.
What this tells us about design
To me, this experience reinforces how important it is to think about limits from the perspective of the entire system, not just one entry point at a time. If multiple domains share the same backend, then rate limits ideally should reflect that shared reality. Otherwise, the system can end up with blind spots that are not obvious until someone actively tests for them.
Closing thoughts
I do not see this as a simple configuration mistake. Instead, it feels more like one of those subtle architectural gaps that only show up under specific conditions. It is the kind of thing that is easy to miss when adding new domains or adjusting traffic flows over time.
Finding and sharing issues like this is meant to help everyone involved. It gives the team a chance to adjust their protections so that limits are applied more consistently across the whole environment. In the long run that leads to a more reliable and resilient system for both users and operators.
The mind map
Here is the mind map I put together as I worked through this. Sometimes small details in headers tell you a lot.