Skip to main content
Redhat Developers  Logo
  • AI

    Get started with AI

    • Red Hat AI
      Accelerate the development and deployment of enterprise AI solutions.
    • AI learning hub
      Explore learning materials and tools, organized by task.
    • AI interactive demos
      Click through scenarios with Red Hat AI, including training LLMs and more.
    • AI/ML learning paths
      Expand your OpenShift AI knowledge using these learning resources.
    • AI quickstarts
      Focused AI use cases designed for fast deployment on Red Hat AI platforms.
    • No-cost AI training
      Foundational Red Hat AI training.

    Featured resources

    • OpenShift AI learning
    • Open source AI for developers
    • AI product application development
    • Open source-powered AI/ML for hybrid cloud
    • AI and Node.js cheat sheet

    Red Hat AI Factory with NVIDIA

    • Red Hat AI Factory with NVIDIA is a co-engineered, enterprise-grade AI solution for building, deploying, and managing AI at scale across hybrid cloud environments.
    • Explore the solution
  • Learn

    Self-guided

    • Documentation
      Find answers, get step-by-step guidance, and learn how to use Red Hat products.
    • Learning paths
      Explore curated walkthroughs for common development tasks.
    • Guided learning
      Receive custom learning paths powered by our AI assistant.
    • See all learning

    Hands-on

    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.
    • Interactive labs
      Learn by doing in these hands-on, browser-based experiences.
    • Interactive demos
      Click through product features in these guided tours.

    Browse by topic

    • AI/ML
    • Automation
    • Java
    • Kubernetes
    • Linux
    • See all topics

    Training & certifications

    • Courses and exams
    • Certifications
    • Skills assessments
    • Red Hat Academy
    • Learning subscription
    • Explore training
  • Build

    Get started

    • Red Hat build of Podman Desktop
      A downloadable, local development hub to experiment with our products and builds.
    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.

    Download products

    • Access product downloads to start building and testing right away.
    • Red Hat Enterprise Linux
    • Red Hat AI
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Featured

    • Red Hat build of OpenJDK
    • Red Hat JBoss Enterprise Application Platform
    • Red Hat OpenShift Dev Spaces
    • Red Hat Developer Toolset

    References

    • E-books
    • Documentation
    • Cheat sheets
    • Architecture center
  • Community

    Get involved

    • Events
    • Live AI events
    • Red Hat Summit
    • Red Hat Accelerators
    • Community discussions

    Follow along

    • Articles & blogs
    • Developer newsletter
    • Videos
    • Github

    Get help

    • Customer service
    • Customer support
    • Regional contacts
    • Find a partner

    Join the Red Hat Developer program

    • Download Red Hat products and project builds, access support documentation, learning content, and more.
    • Explore the benefits

Develop a basic rate limiter with Quarkus and Redis

March 29, 2022
Ana-Maria Mihalceanu
Related topics:
ContainersKubernetesOperatorsQuarkus
Related products:
Developer SandboxRed Hat OpenShift

    Rate limiting is a popular technique to protect services from excessive demand. For example, you can use this strategy to limit how often a user can enter or reset their password. The example in this article builds rate limiting on a per-client basis into a Java-based service for a given time period. The application tracks requests from clients using a Redis in-memory data structure store and limits usage when a client's demand exceeds the quota.

    Here is a broad overview of the implementation:

    1. Boostrap the ratelimit Quarkus project and use the quarkus-redis-client extension to connect to a Redis server.
    2. Create a basic rate limiter that allows users to send 15 greeting requests with the same message in 1 minute.
    3. Test the rate limiter.
    4. Set up a Redis instance using the Redis template in Developer Sandbox for Red Hat OpenShift.
    5. Generate and push a native container image for the ratelimit project.
    6. Deploy to Developer Sandbox or Kubernetes.

    You can obtain the code for the tutorial from its GitHub repository.

    Prerequisites

    Here's what you'll need to follow along with this tutorial:

    • An IDE
    • JDK 11+ with JAVA_HOME configured appropriately
    • Apache Maven 3.8.1+

    To create a native executable, containerize it, and deploy it, you will also need:

    • GraalVM version 21.3.0, configured appropriately. Be sure to install support for Java 11.
    • A working container runtime, such as Podman.
    • Access to a container image registry, such as Red Hat Quay.
    • An account on the Developer Sandbox for Red Hat OpenShift.

    Bootstrap the Quarkus project

    The easiest way to create the Quarkus project is to open a terminal and run the following command:

    $ mvn "io.quarkus:quarkus-maven-plugin:create" \
      -DprojectGroupId="org.acme.example" \
      -DprojectArtifactId="ratelimit" \
      -DprojectVersion="1.0.0-SNAPSHOT" \
      -DclassName="GreeterResource" \
      -Dpath="greeting"

    If you are a Windows user, collapse the command into one line as follows:

    mvn "io.quarkus:quarkus-maven-plugin:create" -DprojectGroupId="org.acme.example" -DprojectArtifactId="ratelimit" -DprojectVersion="1.0.0-SNAPSHOT" -DclassName="GreeterResource" -Dpath="greeting"

    The generated project contains an endpoint labeled GreeterResource and an associated test class named GreeterResourceTest. To finish the setup, go inside the ratelimit directory and add some Quarkus extensions by running:

    $ mvn quarkus:add-extension -Dextensions="quarkus-redis-client,quarkus-container-image-jib,quarkus-kubernetes,quarkus-smallrye-openapi"

    The project is now using the following extensions:

    • quarkus-resteasy to create JSON REST services.
    • quarkus-redis-client to connect to a Redis server. At the time of writing this article, the extension is considered a preview, so please check for improvements.
    • quarkus-container-image-jib to build and push container images.
    • quarkus-kubernetes to generate Kubernetes resources from annotations.
    • quarkus-smallrye-openapi to document the exposed API.

    To validate the project configuration, you can start your Quarkus application by running the following command in a terminal window:

    $ mvn quarkus:dev

    Although you did not provide a Redis host configuration yet, Quarkus enables its Redis Dev Service by default. Furthermore, if there is a working runtime that complies with the Open Container Initiative (OCI) specification, Quarkus will try to configure and start an in-memory data store. You can press r to enable continuous testing, which runs tests immediately after you save your code changes.

    Assuming that your application started in Dev Mode and exposed its service on port 8080, you can check the state of your server by visiting http://localhost:8080/q/swagger-ui/.

    Implement rate limiting

    To integrate GreeterResource requests with Redis, inject the Redis synchronous client (RedisClient) using the following code:

    package org.acme.example;
    
    import io.quarkus.redis.client.RedisClient;
    import org.eclipse.microprofile.config.inject.ConfigProperty;
    
    import javax.inject.Inject;
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    import java.util.logging.Logger;
    
    @Path("greeting")
    public class GreeterResource {
    
        private final static Logger LOGGER = Logger.getLogger(GreeterResource.class.getName());
    
        @Inject
        RedisClient redisClient;
    
        @ConfigProperty(name = "requests.per.minute", defaultValue = "15")
        int customLimit;
    
        @GET
        @Produces(MediaType.TEXT_PLAIN)
        public String hello() {
            return "Hello RESTEasy";
        }
    
    
    }

    To leave some flexibility when setting the limit, the requests.per.minute field is configured with the @ConfigProperty annotation and has a default value of 15.

    Let's build a rate limiter that allows users to send 15 greeting requests per minute containing the same message. When a user attempts the 16th request in a minute, the endpoint replies with HTTP 429 Too Many Requests. Redis documentation provides a best practice that helps with this rate-limiting scenario by using the INCR and EXPIRE commands.

    In the following implementation, the Redis key is the concatenation of the message and the minute when the request is received. If the 15-request limit is reached, the endpoint responds with HTTP 429 TOO MANY REQUEST and sets the time to retry in the X-Rate-Limit-Retry-After-Seconds HTTP header:

    @Path("greeting")
    public class GreeterResource {
    
        private final static Logger LOGGER = Logger.getLogger(GreeterResource.class.getName());
    
        @Inject
        RedisClient redisClient;
    
        @ConfigProperty(name = "requests.per.minute", defaultValue = "15")
        int customLimit;
    
        @GET
        @Produces(MediaType.TEXT_PLAIN)
        @Path("{message}")
        public Response limited(@PathParam(value = "message") String message) {
            final LocalDateTime now = LocalDateTime.now();
            String key = message + ":" + now.getMinute();
            io.vertx.redis.client.Response requests = redisClient.get(key);
            int requestNo = (requests != null) ? requests.toInteger(): 0;
    
            if (requestNo >= customLimit) {
                return  Response.status(javax.ws.rs.core.Response.Status.TOO_MANY_REQUESTS)
                        .header("X-Rate-Limit-Retry-After-Seconds", 60-now.getSecond())
                        .entity(false)
                        .build();
            }
        }
    }

    If the 15 greeting requests limit was not reached, the application proceeds with the following logic:

    • Mark the start of a transaction block with the Redis server using redisClient.multi().
    • Increment the counter for the key by invoking redisClient.incr(key).
    • Set the expiration time for the key to 60 seconds from now using redisClient.expire(key, "60").
    • Execute all previously queued commands in a transaction with redisClient.exec().

    The endpoint should respond with HTTP 200 OK and the number of attempts left until the limit set in the X-Rate-Limit-Remaining header is reached.

    The code for this entire scenario is in the GreeterResource class:

    package org.acme.example;
    
    import io.quarkus.redis.client.RedisClient;
    import org.eclipse.microprofile.config.inject.ConfigProperty;
    
    import javax.ws.rs.core.Response;
    
    import javax.inject.Inject;
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    import java.time.LocalDateTime;
    import java.util.logging.Logger;
    
    @Path("greeting")
    public class GreeterResource {
    
        private final static Logger LOGGER = Logger.getLogger(GreeterResource.class.getName());
    
        @Inject
        RedisClient redisClient;
    
        @ConfigProperty(name = "requests.per.minute", defaultValue = "15")
        int customLimit;
    
        @GET
        @Produces(MediaType.TEXT_PLAIN)
        public String hello() {
            return "Hello RESTEasy";
        }
    
        @GET
        @Produces(MediaType.TEXT_PLAIN)
        @Path("{message}")
        public Response limited(@PathParam(value = "message") String message) {
            final LocalDateTime now = LocalDateTime.now();
            String key = message + ":" + now.getMinute();
            io.vertx.redis.client.Response requests = redisClient.get(key);
            int requestNo = (requests != null) ? requests.toInteger(): 0;
    
            if (requestNo >= customLimit) {
                return  Response.status(javax.ws.rs.core.Response.Status.TOO_MANY_REQUESTS)
                        .header("X-Rate-Limit-Retry-After-Seconds", 60-now.getSecond())
                        .entity(false)
                        .build();
            }
    
            redisClient.multi();
            redisClient.incr(key);
            redisClient.expire(key, "60");
    
            LOGGER.finest(String.format("Request count is %s ", redisClient.exec()));
    
            return Response.status(javax.ws.rs.core.Response.Status.OK)
                    .header("X-Rate-Limit-Remaining", customLimit - requestNo - 1)
                    .entity(true)
                    .build();
        }
    }

    Now that we've built the implementation, let's validate it using tests.

    Test the rate limiter

    For this section, make sure you started Quarkus in Dev Mode and have continuous testing enabled:

    $ mvn quarkus:dev

    Next, add the testLimitedEndpoint method to GreeterResourceTest:

    package org.acme.example;
    
    import io.quarkus.test.junit.QuarkusTest;
    import org.eclipse.microprofile.config.inject.ConfigProperty;
    import org.junit.jupiter.api.Test;
    
    import static io.restassured.RestAssured.given;
    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.CoreMatchers.notNullValue;
    
    @QuarkusTest
    public class GreeterResourceTest {
    
        //inject the limit
        @ConfigProperty(name = "requests.per.minute", defaultValue = "15")
        int customLimit;
    
        @Test
        public void testHelloEndpoint() {
            given()
              .when().get("/greeting")
              .then()
                 .statusCode(200)
                 .body(is("Hello RESTEasy"));
        }
    
        @Test
        public void testLimitedEndpoint() {
            //execute requests within allowed limit
            for (int i=0; i<customLimit; i++) {
                given()
                        .when().get("/greeting/random")
                        .then()
                        .statusCode(200).header("X-Rate-Limit-Remaining", String.valueOf((customLimit - i - 1)))
                        .body(notNullValue());
            }
    
            //execute request after exceeding limit
            given()
                    .when().get("/greeting/random")
                    .then()
                    .statusCode(429).header("X-Rate-Limit-Retry-After-Seconds", notNullValue())
                    .body(notNullValue());
        }
    
    }

    The terminal should display the following message:

    All 2 tests are passing (0 skipped), 2 tests were run in 3748ms. Tests completed at 13:44:17.
    

    Set up a Redis instance in the Developer Sandbox

    To run the ratelimit project in Prod mode, you need a Redis server. You can provision one in your sandbox through the following steps:

    1. In the Developer view, click +Add and go to Developer Catalog - All Services (Figure 1). 

      The Developer Catalog lets you load services in Developer Sandbox.
      Figure 1: The Developer Catalog lets you load services in Developer Sandbox.
    2. In the Developer Catalog search, select Other and type redis. The results will show two Redis templates (Figure 2). The Redis (Ephemeral) template does not offer persistent storage and should be used only for testing. Click on the Redis template without "Ephemeral" in parentheses. 

      There are two Redis templates. Choose the one that is not Ephemeral.
      Figure 2: There are two Redis templates. Choose the one that is not Ephemeral.
    3. Click the Instantiate Template button.
    4. In the template, fill in the database name and password with the string redis (Figure 3). 

      Fill in the database service name and password.
      Figure 3: Fill in the database service name and password.
    5. Click the Create button.

    The topology view now shows the redis DeploymentConfig resource. You can click on it to see its content (Figure 4).

    A pod and service are shown for Redis.
    Figure 4: A pod and service are shown for Redis.

    In this view, click on the pod name to view Pod details. In the Pod details view, select the Environment tab to see the Kubernetes secret name that holds the database password (Figure 5). You will use this secret and password in the next section.

    The secret and password are shown in Pod details.
    Figure 5: The secret and password are shown in Pod details.

    Generate and push container images using Jib

    Your container will be a Docker image built with Jib. Before building and pushing any container images, you should customize the ratelimit project for production mode. The Redis Dev Service helped when running the application in Dev mode, but you should now add Redis server configurations for Prod mode in src/main/resources/application.properties:

    custom.host=${REDIS_HOST:redis}
    
    ###use the secret name and key seen in previous section
    quarkus.kubernetes.env.mapping.redis.from-secret=redis
    quarkus.kubernetes.env.mapping.redis.with-key=database-password
    
    %prod.quarkus.redis.hosts=redis://${custom.host}:6379
    %prod.quarkus.redis.password=${quarkus.kubernetes.env.mapping.redis.with-key:redis}

    Note: When connecting to data sources, avoid hardcoding URLs or passwords in configuration files. Instead, parameterize the data in configuration entries such as $custom.host and $quarkus.kubernetes.env.mapping.redis.with-key. You can overwrite the values when packaging the application.

    To build and push the container image for ratelimit to your container registry, customize some details in src/main/resources/application.properties:

    #set your registry and image details
    quarkus.container-image.registry=quay.io
    quarkus.container-image.group=yourrepo
    quarkus.container-image.name=greeting-app
    quarkus.container-image.tag=1.0.10-native
    
    #automatically pushes the image when packaging the app
    quarkus.container-image.push=true
    
    
    #base native image to be used when producing a container image at jar build
    quarkus.jib.base-native-image=registry.access.redhat.com/ubi8/ubi-minimal:8.5

    If your local operating system is not based on Linux when packaging to native, you should also specify the docker image to use for the local build:

    $ mvn package -Pnative -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:21.3-java11
    

    This step is necessary to produce a build for a Linux binary. Without it, you'll get the following error when packaging or running the container:

    The native binary produced by the build is not a Linux binary and therefore cannot be used in a Linux container image.

    Deploy to Kubernetes

    Using the quarkus-kubernetes extension, Quarkus can generate Kubernetes resources based on annotations or user-given configurations. By applying the generated manifests, you can deploy the application to a target Kubernetes cluster, such as Developer Sandbox.

    Because the ratelimit project integrates with a Redis server, you can customize the generated Kubernetes resources to use Redis details as environment variables in src/main/resources/application.properties:

    #expose application via a LoadBalancer Kubernetes service
    quarkus.kubernetes.service-type=load-balancer
    
    #associate an ingress with the service
    quarkus.kubernetes.ingress.expose=true
    
    #automatically deploy to Kubernetes when packaging the app
    quarkus.kubernetes.deploy=true
    
    
    #use Kubernetes Service named redis
    custom.host=${REDIS_HOST:redis}
    quarkus.kubernetes.env.vars.redis-host=${custom.host}
    
    #get Redis password from secret with name redis
    quarkus.kubernetes.env.mapping.redis.from-secret=redis
    quarkus.kubernetes.env.mapping.redis.with-key=database-password
    
    
    #reuse previously defined values
    %prod.quarkus.redis.hosts=redis://${custom.host}:6379
    %prod.quarkus.redis.password=${quarkus.kubernetes.env.mapping.redis.with-key:redis}

    You can check the resources generated in target/kubernetes/kubernetes.yml and apply those into your cluster via the kubectl command-line interface (CLI) or when packaging the application:

    $ mvn package -Pnative -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:21.3-java11
    

    After successfully deploying the resources, you can open a terminal within your browser by clicking on the >_ sign in the upper right menu (Figure 6).

    The terminal is available through a link on the upper right side of the sandbox interface.
    Figure 6: The terminal is available through a link on the upper right side of the sandbox interface.

    The browser terminal has a few CLI tools installed, such as oc, kubectl, and helm. Execute the following line in the terminal to export the URL associated with the recently deployed resources:

    $ export URL=$( kubectl get service greeting-app  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')

    Next, let's invoke the /greeting/{message} endpoint 16 times in the same minute and use the same message value to observe the rate limiter's response:

    $ for i in {1..16}; do curl -v $URL/greeting/hello; done

    Next up

    The quarkus-redis-client extension makes it easier for you to interact with a Redis server and use commands specific to it (GET, MULTI, INCR, EXPIRE). Quarkus Dev Services automatically provisions a data source without any configuration, and you can test the rate-limiting behavior while having Quarkus continuous testing enabled.

    Once you've finished this tutorial, you can clean up your sandbox by running the following lines in the browser terminal:

    $ curl https://raw.githubusercontent.com/ammbra/ratelimit/master/sandbox-cleaner.sh -o sandbox-cleaner.sh
    $ sh sandbox-cleaner.sh

    You can further play with the code by changing the limit, pushing the image, and deploying it as many times as you want on your sandbox.

    Last updated: September 25, 2024

    Related Posts

    • Test-driven development with Quarkus

    • Explore Java 17 language features with Quarkus

    • Red Hat build of Quarkus 2.2: Simplified Kubernetes-native Java

    Recent Posts

    • Every layer counts: Defense in depth for AI agents with Red Hat AI

    • Fun in the RUN instruction: Why container builds with distroless images can surprise you

    • Trusted software factory: Building trust in the agentic AI era

    • Build a zero trust AI pipeline with OpenShift and RHEL CVMs

    • Red Hat Hardened Images: Top 5 benefits for software developers

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Platforms

    • Red Hat AI
    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Build

    • Developer Sandbox
    • Developer tools
    • Interactive tutorials
    • API catalog

    Quicklinks

    • Learning resources
    • E-books
    • Cheat sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site status dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2026 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Chat Support

    Please log in with your Red Hat account to access chat support.