In my talk, “Evolving your APIs,” I mention that an API gateway is a Reverse Proxy "on steroids.” One key difference between the former and the latter is that the API Gateway is not unfriendly to business logic. The poster child is rate-limiting. Rate-limiting is an age-old Reverse Proxy feature focused on protecting against Distributed Denial of Service (DDoS) attacks. It treats all clients the same and is purely technical. In this day and age, most API providers offer different subscription tiers; the higher the tier, the higher the rate limit, and the more you pay incidentally. It's not technical anymore and requires to differentiate between clients. In this post, I want to detail how to do it with Apache APISIX. Note I take most of the material from the workshop. Rate-limiting for the masses Apache APISIX offers no less than three plugins to rate limit requests: limit conn: limits the number of concurrent requests limit req: limits the number of requests based on the Leaky Bucket algorithm limit count: limits the number of requests based on a fixed time window The limit-count plugin is a good candidate for this post. Let's configure the plugin for a route: routes: - uri: /get upstream: nodes: "http://httpbin.org:80": 1 plugins: limit-count: #1 count: 1 #2 time_window: 60 #2 rejected_code: 429 #3 #END Set the limit-count plugin Limit requests to one every 60 seconds Override the default HTTP response code, i.e., 503 At this point, we configured regular rate limiting. curl -v http://localhost:9080/get curl -v http://localhost:9080/get If we execute the second request before a minute has passed, the result is the following: HTTP/1.1 429 Too Many Requests Date: Tue, 09 Jul 2024 06:55:07 GMT Content-Type: text/html; charset=utf-8 Content-Length: 241 Connection: keep-alive X-RateLimit-Limit: 1 #1 X-RateLimit-Remaining: 0 #2 X-RateLimit-Reset: 59 #3 Server: APISIX/3.9.1 <html> <head><title>429 Too Many Requests</title></head> <body> <center><h1>429 Too Many Requests</h1></center> <hr><center>openresty</center> <p><em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em></p></body> </html> Configured limit Remaining quota Waiting time in seconds before quota replenishment Per-consumer rate limiting To configure per-consumer rate limiting, we first need to implement request authentication. APISIX offers many authentication plugins; we shall use the simplest one, key-auth. key-auth checks a specific HTTP request header - apikey by default. Here's how we configure consumers: consumers: - username: johndoe #1 plugins: key-auth: key: john #2 - username: janedoe #1 plugins: key-auth: key: jane #2 Users HTTP header request value curl -H 'apikey: john' localhost:9080/get #1 curl -H 'apikey: jane' localhost:9080/get #2 Authenticate as johndoe Authenticate as janedoe In general, you attach plugins to APISIX routes but can also attach them to consumers. We can now move the limit-count plugin. routes: - uri: /get upstream: nodes: "httpbin:80": 1 plugins: key-auth: ~ #1 consumers: - username: johndoe plugins: key-auth: key: john limit-count: count: 1 #2 time_window: 60 rejected_code: 429 - username: janedoe plugins: key-auth: key: jane limit-count: count: 5 #2 time_window: 60 rejected_code: 429 #END The route is only accessible to requests authenticating with key-auth johndoe has a lower limit count than janedoe. Did he forget to pay his subscription fees? curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get The second request gets rate-limited. Per-group rate limiting We never attach permissions directly to identities in Identity Management systems. It's considered bad practice because when a person moves around the organization, we need to add and remove permissions one by one. The good practice is to attach permissions to groups and set the person in that group. When the person moves, we change their group; the person loses permissions from the old group and gets permissions from the new group. People get their permissions transitively via their groups. Apache APISIX offers an abstraction called a Consumer Group for this. Let's create two consumer groups with different rate limit values: consumer_groups: - id: 1 plugins: limit-count: count: 1 time_window: 60 rejected_code: 429 - id: 2 plugins: limit-count: count: 5 time_window: 60 rejected_code: 429 The next step is to attach consumers to these groups: consumers: - username: johndoe group_id: 1 plugins: key-auth: key: john - username: janedoe group_id: 2 plugins: key-auth: key: jane curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get The second request gets rate-limited. We have the same results as before with two benefits. The first one is as I wrote above: when consumers move in and out, they change their permissions accordingly. The second benefit is that the limit count is shared among all consumers of a group. Indeed, when you set a limit, you don't want each consumer to be rate limited at X requests per Y second; you want the group as a whole to share the limit. In this way, if a single consumer is very active, they will naturally cap the rate of other consumers who share the same group. Of course, you can set a limit on both a consumer and the group it belongs to. In this case, the lowest limit will apply first. consumers: - username: johndoe group_id: 2 #1 plugins: key-auth: key: john limit-count: count: 1 #2 time_window: 60 rejected_code: 429 - username: janedoe group_id: 2 plugins: key-auth: key: jane Move johndoe to group 2 Limit him individually curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get #1 johndoe hits the limit here, but janedoe now only has four requests left from this minute, as the former used one request Conclusion In this post, we implement rate limiting with Apache APISIX. We set the rate limit on a route and moved it to individual consumers. Then we moved it to consumer groups, so all consumers in a group share the same "pool". The complete source code for this post can be found on GitHub. To go further: Consumer Consumer Group Apache APISIX Hands-on Lab In my talk, “Evolving your APIs,” I mention that an API gateway is a Reverse Proxy "on steroids.” One key difference between the former and the latter is that the API Gateway is not unfriendly to business logic. The poster child is rate-limiting. Rate-limiting is an age-old Reverse Proxy feature focused on protecting against Distributed Denial of Service (DDoS) attacks. It treats all clients the same and is purely technical. In this day and age, most API providers offer different subscription tiers; the higher the tier, the higher the rate limit, and the more you pay incidentally. It's not technical anymore and requires to differentiate between clients. In this post, I want to detail how to do it with Apache APISIX. Note I take most of the material from the workshop . workshop Rate-limiting for the masses Apache APISIX offers no less than three plugins to rate limit requests: limit conn: limits the number of concurrent requests limit req: limits the number of requests based on the Leaky Bucket algorithm limit count: limits the number of requests based on a fixed time window limit conn : limits the number of concurrent requests limit conn limit req : limits the number of requests based on the Leaky Bucket algorithm limit req Leaky Bucket limit count : limits the number of requests based on a fixed time window limit count The limit-count plugin is a good candidate for this post. limit-count Let's configure the plugin for a route: routes: - uri: /get upstream: nodes: "http://httpbin.org:80": 1 plugins: limit-count: #1 count: 1 #2 time_window: 60 #2 rejected_code: 429 #3 #END routes: - uri: /get upstream: nodes: "http://httpbin.org:80": 1 plugins: limit-count: #1 count: 1 #2 time_window: 60 #2 rejected_code: 429 #3 #END Set the limit-count plugin Limit requests to one every 60 seconds Override the default HTTP response code, i.e., 503 Set the limit-count plugin limit-count Limit requests to one every 60 seconds Override the default HTTP response code, i.e. , 503 i.e. 503 At this point, we configured regular rate limiting. curl -v http://localhost:9080/get curl -v http://localhost:9080/get curl -v http://localhost:9080/get curl -v http://localhost:9080/get If we execute the second request before a minute has passed, the result is the following: HTTP/1.1 429 Too Many Requests Date: Tue, 09 Jul 2024 06:55:07 GMT Content-Type: text/html; charset=utf-8 Content-Length: 241 Connection: keep-alive X-RateLimit-Limit: 1 #1 X-RateLimit-Remaining: 0 #2 X-RateLimit-Reset: 59 #3 Server: APISIX/3.9.1 <html> <head><title>429 Too Many Requests</title></head> <body> <center><h1>429 Too Many Requests</h1></center> <hr><center>openresty</center> <p><em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em></p></body> </html> HTTP/1.1 429 Too Many Requests Date: Tue, 09 Jul 2024 06:55:07 GMT Content-Type: text/html; charset=utf-8 Content-Length: 241 Connection: keep-alive X-RateLimit-Limit: 1 #1 X-RateLimit-Remaining: 0 #2 X-RateLimit-Reset: 59 #3 Server: APISIX/3.9.1 <html> <head><title>429 Too Many Requests</title></head> <body> <center><h1>429 Too Many Requests</h1></center> <hr><center>openresty</center> <p><em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em></p></body> </html> Configured limit Remaining quota Waiting time in seconds before quota replenishment Configured limit Remaining quota Waiting time in seconds before quota replenishment Per-consumer rate limiting To configure per-consumer rate limiting, we first need to implement request authentication. APISIX offers many authentication plugins; we shall use the simplest one, key-auth . key-auth checks a specific HTTP request header - apikey by default. key-auth key-auth apikey Here's how we configure consumers: consumers: - username: johndoe #1 plugins: key-auth: key: john #2 - username: janedoe #1 plugins: key-auth: key: jane #2 consumers: - username: johndoe #1 plugins: key-auth: key: john #2 - username: janedoe #1 plugins: key-auth: key: jane #2 Users HTTP header request value Users HTTP header request value curl -H 'apikey: john' localhost:9080/get #1 curl -H 'apikey: jane' localhost:9080/get #2 curl -H 'apikey: john' localhost:9080/get #1 curl -H 'apikey: jane' localhost:9080/get #2 Authenticate as johndoe Authenticate as janedoe Authenticate as johndoe johndoe Authenticate as janedoe janedoe In general, you attach plugins to APISIX routes but can also attach them to consumers. We can now move the limit-count plugin. limit-count routes: - uri: /get upstream: nodes: "httpbin:80": 1 plugins: key-auth: ~ #1 consumers: - username: johndoe plugins: key-auth: key: john limit-count: count: 1 #2 time_window: 60 rejected_code: 429 - username: janedoe plugins: key-auth: key: jane limit-count: count: 5 #2 time_window: 60 rejected_code: 429 #END routes: - uri: /get upstream: nodes: "httpbin:80": 1 plugins: key-auth: ~ #1 consumers: - username: johndoe plugins: key-auth: key: john limit-count: count: 1 #2 time_window: 60 rejected_code: 429 - username: janedoe plugins: key-auth: key: jane limit-count: count: 5 #2 time_window: 60 rejected_code: 429 #END The route is only accessible to requests authenticating with key-auth johndoe has a lower limit count than janedoe. Did he forget to pay his subscription fees? The route is only accessible to requests authenticating with key-auth key-auth johndoe has a lower limit count than janedoe . Did he forget to pay his subscription fees? johndoe janedoe curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get The second request gets rate-limited. Per-group rate limiting We never attach permissions directly to identities in Identity Management systems. It's considered bad practice because when a person moves around the organization, we need to add and remove permissions one by one. The good practice is to attach permissions to groups and set the person in that group. When the person moves, we change their group; the person loses permissions from the old group and gets permissions from the new group. People get their permissions transitively via their groups. transitively Apache APISIX offers an abstraction called a Consumer Group for this. Consumer Group Let's create two consumer groups with different rate limit values: consumer_groups: - id: 1 plugins: limit-count: count: 1 time_window: 60 rejected_code: 429 - id: 2 plugins: limit-count: count: 5 time_window: 60 rejected_code: 429 consumer_groups: - id: 1 plugins: limit-count: count: 1 time_window: 60 rejected_code: 429 - id: 2 plugins: limit-count: count: 5 time_window: 60 rejected_code: 429 The next step is to attach consumers to these groups: consumers: - username: johndoe group_id: 1 plugins: key-auth: key: john - username: janedoe group_id: 2 plugins: key-auth: key: jane consumers: - username: johndoe group_id: 1 plugins: key-auth: key: john - username: janedoe group_id: 2 plugins: key-auth: key: jane curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get curl -H 'apikey: jane' localhost:9080/get The second request gets rate-limited. We have the same results as before with two benefits. The first one is as I wrote above: when consumers move in and out, they change their permissions accordingly. The second benefit is that the limit count is shared among all consumers of a group. Indeed, when you set a limit, you don't want each consumer to be rate limited at X requests per Y second; you want the group as a whole to share the limit. In this way, if a single consumer is very active, they will naturally cap the rate of other consumers who share the same group. Of course, you can set a limit on both a consumer and the group it belongs to. In this case, the lowest limit will apply first. consumers: - username: johndoe group_id: 2 #1 plugins: key-auth: key: john limit-count: count: 1 #2 time_window: 60 rejected_code: 429 - username: janedoe group_id: 2 plugins: key-auth: key: jane consumers: - username: johndoe group_id: 2 #1 plugins: key-auth: key: john limit-count: count: 1 #2 time_window: 60 rejected_code: 429 - username: janedoe group_id: 2 plugins: key-auth: key: jane Move johndoe to group 2 Limit him individually Move johndoe to group 2 johndoe Limit him individually curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get #1 curl -H 'apikey: john' localhost:9080/get curl -H 'apikey: john' localhost:9080/get #1 johndoe hits the limit here, but janedoe now only has four requests left from this minute, as the former used one request johndoe hits the limit here, but janedoe now only has four requests left from this minute, as the former used one request johndoe janedoe Conclusion In this post, we implement rate limiting with Apache APISIX. We set the rate limit on a route and moved it to individual consumers. Then we moved it to consumer groups, so all consumers in a group share the same "pool". The complete source code for this post can be found on GitHub . GitHub To go further: To go further: Consumer Consumer Group Apache APISIX Hands-on Lab Consumer Consumer Consumer Group Consumer Group Apache APISIX Hands-on Lab Apache APISIX Hands-on Lab