Rate Limiter With Resilience4j
Sometimes we consume APIs that are rate limited, in this article you'll see how to explain your own system to play the rate limit rules. Enough words, better show me the code!
Intro
In this article, I am going to show by example how to use a rate limiter when consuming external APIs. Why would we need it? API providers may set rate limits for control of traffic in an attempt to stay performant and/or defend from DoS attacks. From the API consumption side, even if API is not rate limited we just might want to ping API with some frequency, like checking the currency exchange rate once a minute instead of bombarding the endpoint with redundant requests.
Code
All the code of the example can be found in the GitHub repo. It is in Kotlin (and without Spring). With Spring it would be somewhat faster to get going as you will need to configure your rate limiter in the application properties and then reference your rate limiter in annotation over the target function.
API
I chose the minimum setup API — https://www.freeforexapi.com/api/live It gives us information about all supported currency pairs that we can specify as query parameters. For example, https://www.freeforexapi.com/api/live?pairs=EURUSD would give us the exchange rate for the currency pair EUR/USD. Let’s say we want to ping it once every 3 seconds.
Let’s code
Calling API
We need the general ability to call the API, for that, I’ve used khttp library which proved to be super easy to use, see for yourself:
fun externalCallToGetExchangeRate(currencyPair: String) {
val response = khttp.get(
url = FOREX_API_BASE_URL,
params = mapOf("pairs" to currencyPair)
).text
val parsedRateFromResponse = response.split("rate\":")[1].split(",")[0]
logger.info("$currencyPair $parsedRateFromResponse")
}
companion object {
const val FOREX_API_BASE_URL = "https://www.freeforexapi.com/api/live"
val logger: Logger = LoggerFactory.getLogger(Demo::class.java)
}
I am using a logger here to be able to see when exactly calls are happening to check if rate limiting actually works. For setup of the logger, feel free to check the repository, mentioned above. Plus here you saw a bit of awkward response parsing without normally deserializing it, this is just for brevity ;)
Resilience4j setup
We are already able to call external service. Let’s configure the rate limiter using Resilience4j which besides rate limiting provides circuit breaker and retry failed calls functionalities. Configuring rate limiting as far as I can tell wasn’t really straightforward:
fun setupRateLimiter(): RateLimiter {
val config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(3))
.limitForPeriod(1)
.build()
val rateLimiterRegistry = RateLimiterRegistry.of(config)
return rateLimiterRegistry.rateLimiter("someNameOfRateLimiter")
}
So, here we create a configuration with the requirements we had: for a period of 3 seconds to limit execution to one time only. Then we create a registry specifying this configuration and only after we can have the rate limiter. How to wire it with our API call is what we are going to do next. But before that, if you’d set up limits differently, let’s say once a minute you’d hit the exception:
Exception in thread "main" io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'someNameOfRateLimiter' does not permit further calls
at io.github.resilience4j.ratelimiter.RequestNotPermitted.createRequestNotPermitted(RequestNotPermitted.java:43)
at io.github.resilience4j.ratelimiter.RateLimiter.waitForPermission(RateLimiter.java:511)
at io.github.resilience4j.ratelimiter.RateLimiter.lambda$decorateConsumer$9(RateLimiter.java:389)
What to do in this case? We should also add the timeout value, which basically means how long we want to wait for permission to do the next call. Our remote call is rather fast and rare — only once in a period, so we need to make our thread a bit more “patient” and let it know to wait. If we set our period to 1 minute and don’t want to see this exception of an impatient thread we can specify 1 minute timeout value. For our current use case with 3 seconds period if our call execution would be too fast, let’s add it too:
val config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(3))
.limitForPeriod(1)
.timeoutDuration(Duration.ofSeconds(3))
.build()
Wrapper
Now the last step of configuration. We are going to write a small wrapper of our function which makes an API call. It can be any function, so rate limiting would work for literally anything.
fun main() {
val demo = Demo()
val myRateLimiter = demo.setupRateLimiter()
val restrictedCall = RateLimiter.decorateConsumer<String>(
myRateLimiter
) { currencyPair -> demo.externalCallToGetExchangeRate(currencyPair) }
}
In the code above we wire things together. Getting our rate limiter prepared and then decorating our target function with it brings us to the last moment, where we actually trigger the functionality. Let’s add a bit of code to the previous snippet:
while (true) {
restrictedCall.accept("EURUSD")
}
Just a note on function decoration, it works well with popular functional interfaces: Consumer, Supplier, Function. In this case, we had a function that had an input but no output (the result was just printed out) so Consumer was the choice.
Running the code
As a result of running the code, you will see something like this:
As you can see it is not extremely precise when you look at each individual call, sometimes it is 4 seconds between the calls, sometimes 2, but if you “zoom” out you’d see we get our call every 3 seconds (like 4 is followed by 2 for example).
Happy coding!