Lots of service providers offer a free tier of their service. The idea is to let you kick their service's tires freely. If you need to go above the free tier at any point, you'll likely stay on the service and pay. In this day and age, most services are online and accessible via an API. Today, we will implement a free tier with Apache APISIX. A naive approach I implemented a free tier in my post Evolving your RESTful APIs, a step-by-step approach, albeit in a very naive way. I copy-pasted the limit-count plugin and added my required logic. function _M.access(conf, ctx) core.log.info("ver: ", ctx.conf_version) -- no limit if the request is authenticated local key = core.request.header(ctx, conf.header) #1 if key then local consumer_conf = consumer_mod.plugin("key-auth") #2 if consumer_conf then local consumers = lrucache("consumers_key", consumer_conf.conf_version, #3 create_consume_cache, consumer_conf) local consumer = consumers[key] #4 if consumer then #5 return end end end -- rest of the logic Get the configured request header value Get the consumer's key-auth configuration Get consumers Get the consumer with the passed API key if they exist If they exist, bypass the rate-limiting logic The downside of this approach is that the code is now my own. It has evolved since I copied it, and I'm stuck with the version I copied. We can do better with the help of the vars parameter on routes. APISIX route matching APISIX delegates its matching rule to a router. Standard matching parameters include: The URI The HTTP method. By default, all methods match. The host The remote address Most users do match on the URI; a small minority use HTTP methods and the host. However, they are not the only matching parameters. Knowing the rest will bring you into the world of advanced users of APISIX. Let's take a simple example: header-based API versioning. You'd need actually to match a specific HTTP request header. I've already described how to do it previously. In essence, vars is an additional matching criterion that allows the evaluation of APISIX and nginx variables. routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] The above route will match if, and only if, the HTTP Accept header is equal to vnd.ch.frankel.myservice.v1+json. You can find the complete list of supported operators in the lua-resty-expr project. APISIX matches routes in a non-specified order by default. If URIs are disjointed, that's not an issue. routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] Problems arise when URIs are somehow not disjointed. For example, imagine I want to set a default route for unversioned calls. routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] - uri: /* upstream_id: 1 We need the third route to be evaluated last. If it's evaluated first, it will match all requests, regardless of their HTTP headers. APISIX offers the priority parameter to order route evaluation. By default, a route's priority is 0. Let's use priority to implement the versioning correctly: routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] priority: 10 #1 - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] priority: 10 #1 - uri: /* upstream_id: 1 Evaluated first. The order is not relevant since the URIs are disjointed. Implementing a free tier with matching rules We are now ready to implement our free tier with matching rules. The first route to be evaluated should be the one with authentication and no rate limit: routes: - uri: /get upstream_id: 1 vars: [[ "http_apikey", "~~", ".*"]] #1 plugins: key-auth: ~ #2 priority: 10 #3 Match if the request has an HTTP header named apikey Authenticate the request Evaluate first The other route is evaluated afterward. - uri: /get upstream_id: 1 plugins: limit-count: #1 count: 1 time_window: 60 rejected_code: 429 Rate limit this route. When you configure APISIX with the above snippets, and it receives a request to /get, it tries to match the first route only if it has an apikey request header: If it has one: The key-auth plugin kicks in If it succeeds, APISIX forwards the request to the upstream If it fails, APISIX returns a 403 HTTP status code If it has no such request header, it matches the second route with a rate limit. Conclusion A free tier is a must for any API service provider worth its salt. In this post, I've explained how to configure such free tier with Apache APISIX. The complete source code for this post can be found on GitHub: https://github.com/ajavageek/free-tier-apisix?embedable=true To go further: Consumer limit-count plugin router-radixtree lua-resty-expr Lots of service providers offer a free tier of their service. The idea is to let you kick their service's tires freely. If you need to go above the free tier at any point, you'll likely stay on the service and pay. In this day and age, most services are online and accessible via an API. Today, we will implement a free tier with Apache APISIX . Apache APISIX A naive approach I implemented a free tier in my post Evolving your RESTful APIs, a step-by-step approach , albeit in a very naive way. I copy-pasted the limit-count plugin and added my required logic. Evolving your RESTful APIs, a step-by-step approach limit-count function _M.access(conf, ctx) core.log.info("ver: ", ctx.conf_version) -- no limit if the request is authenticated local key = core.request.header(ctx, conf.header) #1 if key then local consumer_conf = consumer_mod.plugin("key-auth") #2 if consumer_conf then local consumers = lrucache("consumers_key", consumer_conf.conf_version, #3 create_consume_cache, consumer_conf) local consumer = consumers[key] #4 if consumer then #5 return end end end -- rest of the logic function _M.access(conf, ctx) core.log.info("ver: ", ctx.conf_version) -- no limit if the request is authenticated local key = core.request.header(ctx, conf.header) #1 if key then local consumer_conf = consumer_mod.plugin("key-auth") #2 if consumer_conf then local consumers = lrucache("consumers_key", consumer_conf.conf_version, #3 create_consume_cache, consumer_conf) local consumer = consumers[key] #4 if consumer then #5 return end end end -- rest of the logic Get the configured request header value Get the consumer's key-auth configuration Get consumers Get the consumer with the passed API key if they exist If they exist, bypass the rate-limiting logic Get the configured request header value Get the consumer's key-auth configuration key-auth Get consumers Get the consumer with the passed API key if they exist If they exist, bypass the rate-limiting logic The downside of this approach is that the code is now my own. It has evolved since I copied it, and I'm stuck with the version I copied. We can do better with the help of the vars parameter on routes. vars APISIX route matching APISIX delegates its matching rule to a router . router Standard matching parameters include: The URI The HTTP method. By default, all methods match. The host The remote address The URI The HTTP method. By default, all methods match. The host The remote address Most users do match on the URI; a small minority use HTTP methods and the host. However, they are not the only matching parameters. Knowing the rest will bring you into the world of advanced users of APISIX. Let's take a simple example: header-based API versioning. You'd need actually to match a specific HTTP request header. I've already described how to do it previously . In essence, vars is an additional matching criterion that allows the evaluation of APISIX and nginx variables. previously vars routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] The above route will match if, and only if, the HTTP Accept header is equal to vnd.ch.frankel.myservice.v1+json . You can find the complete list of supported operators in the lua-resty-expr project. Accept vnd.ch.frankel.myservice.v1+json lua-resty-expr APISIX matches routes in a non-specified order by default. If URIs are disjointed , that's not an issue. disjointed routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] Problems arise when URIs are somehow not disjointed. For example, imagine I want to set a default route for unversioned calls. routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] - uri: /* upstream_id: 1 routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] - uri: /* upstream_id: 1 We need the third route to be evaluated last. If it's evaluated first, it will match all requests, regardless of their HTTP headers. APISIX offers the priority parameter to order route evaluation. By default, a route's priority is 0. Let's use priority to implement the versioning correctly: priority priority routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] priority: 10 #1 - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] priority: 10 #1 - uri: /* upstream_id: 1 routes: - uri: /* upstream_id: 1 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v1+json" ]] priority: 10 #1 - uri: /* upstream_id: 2 vars: [[ "http_accept", "==", "vnd.ch.frankel.myservice.v2+json" ]] priority: 10 #1 - uri: /* upstream_id: 1 Evaluated first. The order is not relevant since the URIs are disjointed. Evaluated first. The order is not relevant since the URIs are disjointed. Implementing a free tier with matching rules We are now ready to implement our free tier with matching rules. The first route to be evaluated should be the one with authentication and no rate limit: routes: - uri: /get upstream_id: 1 vars: [[ "http_apikey", "~~", ".*"]] #1 plugins: key-auth: ~ #2 priority: 10 #3 routes: - uri: /get upstream_id: 1 vars: [[ "http_apikey", "~~", ".*"]] #1 plugins: key-auth: ~ #2 priority: 10 #3 Match if the request has an HTTP header named apikey Authenticate the request Evaluate first Match if the request has an HTTP header named apikey apikey Authenticate the request Evaluate first The other route is evaluated afterward. - uri: /get upstream_id: 1 plugins: limit-count: #1 count: 1 time_window: 60 rejected_code: 429 - uri: /get upstream_id: 1 plugins: limit-count: #1 count: 1 time_window: 60 rejected_code: 429 Rate limit this route. Rate limit this route. When you configure APISIX with the above snippets, and it receives a request to /get , it tries to match the first route only if it has an apikey request header: /get only apikey If it has one: The key-auth plugin kicks in If it succeeds, APISIX forwards the request to the upstream If it fails, APISIX returns a 403 HTTP status code If it has no such request header, it matches the second route with a rate limit. If it has one: The key-auth plugin kicks in If it succeeds, APISIX forwards the request to the upstream If it fails, APISIX returns a 403 HTTP status code The key-auth plugin kicks in If it succeeds, APISIX forwards the request to the upstream If it fails, APISIX returns a 403 HTTP status code The key-auth plugin kicks in key-auth If it succeeds, APISIX forwards the request to the upstream If it fails, APISIX returns a 403 HTTP status code If it has no such request header, it matches the second route with a rate limit. Conclusion A free tier is a must for any API service provider worth its salt. In this post, I've explained how to configure such free tier with Apache APISIX. The complete source code for this post can be found on GitHub: https://github.com/ajavageek/free-tier-apisix?embedable=true https://github.com/ajavageek/free-tier-apisix?embedable=true To go further: To go further: Consumer limit-count plugin router-radixtree lua-resty-expr Consumer Consumer limit-count plugin limit-count plugin router-radixtree router-radixtree lua-resty-expr lua-resty-expr