0Pricing
Spring Security 6 & JWT Authentication · Lesson

Rate Limiting API Access

Protect your authentication endpoints and APIs from brute-force and denial-of-service attacks using rate limiting.

Rate Limiting API Access is a free Spring Security 6 & JWT Authentication lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Spring Security 6 & JWT Authentication learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What is Rate Limiting?

Rate limiting is a crucial security measure that controls how often a user or system can repeat an action within a certain timeframe.

Think of it as a bouncer at a club, allowing only a certain number of people in per minute to prevent overcrowding.

It's vital for protecting your APIs and authentication endpoints from malicious activities.

Why APIs Need Rate Limiting

Without rate limiting, your application is vulnerable to several attacks:

  • Brute-Force Attacks: Repeated login attempts to guess passwords.
  • Denial-of-Service (DoS): Overwhelming your server with requests to make it unavailable.
  • Resource Exhaustion: Legitimate users might suffer if one user consumes too many resources.

Rate limiting helps maintain stability and security.

Rate Limiting Strategies

There are several ways to implement rate limiting:

  • Fixed Window: Allows N requests per window (e.g., 100 requests per minute). Simple, but can have a "burst" problem at window edges.
  • Sliding Window: A more sophisticated approach that smooths out traffic, preventing bursts.
  • Token Bucket/Leaky Bucket: More complex, often used for traffic shaping.

For our lesson, we'll focus on a simple fixed window approach.

Spring Boot Implementation

In Spring Boot, we can implement rate limiting using a custom Servlet Filter.

A filter intercepts incoming HTTP requests before they reach your controller methods. This allows us to check the request's rate limit and block it if necessary.

We'll create a dedicated service to manage the rate limiting logic.

Basic Rate Limiter Service

First, let's create a simple service to track request counts. This example uses an in-memory map for demonstration.

Try running this basic service setup:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class RateLimiterService {

    private final int MAX_REQUESTS;
    private final long TIME_WINDOW_MILLIS; // e.g., 1 minute = 60000ms

    // Stores request counts per client (e.g., IP address) within a window
    private final ConcurrentHashMap<String, RequestInfo> requestCounts = new ConcurrentHashMap<>();

    public RateLimiterService(int maxRequests, long timeWindowMillis) {
        this.MAX_REQUESTS = maxRequests;
        this.TIME_WINDOW_MILLIS = timeWindowMillis;
    }

    public boolean tryAcquire(String clientIdentifier) {
        long currentTime = System.currentTimeMillis();

        requestCounts.compute(clientIdentifier, (key, info) -> {
            if (info == null || currentTime - info.windowStartTime > TIME_WINDOW_MILLIS) {
                // New window or old window expired
                return new RequestInfo(currentTime, 1);
            } else {
                // Same window, increment count
                info.requestCount.incrementAndGet();
                return info;
            }
        });

        return requestCounts.get(clientIdentifier).requestCount.get() <= MAX_REQUESTS;
    }

    private static class RequestInfo {
        long windowStartTime;
        AtomicInteger requestCount;

        RequestInfo(long windowStartTime, int initialCount) {
            this.windowStartTime = windowStartTime;
            this.requestCount = new AtomicInteger(initialCount);
        }
    }

    public static void main(String[] args) {
        RateLimiterService limiter = new RateLimiterService(3, 10000); // 3 requests per 10 seconds

        System.out.println("Client A - Attempt 1: " + limiter.tryAcquire("clientA"));
        System.out.println("Client A - Attempt 2: " + limiter.tryAcquire("clientA"));
        System.out.println("Client A - Attempt 3: " + limiter.tryAcquire("clientA"));
        System.out.println("Client A - Attempt 4: " + limiter.tryAcquire("clientA")); // Should be false
    }
}

Understanding the Service

Our RateLimiterService uses a ConcurrentHashMap to store RequestInfo for each client identifier (like an IP address).

  • MAX_REQUESTS: The maximum number of requests allowed.
  • TIME_WINDOW_MILLIS: The duration of the fixed window.
  • tryAcquire(String clientIdentifier): This method checks if a client can make a request. It resets the window if expired or increments the count.

The main method demonstrates its usage.

Creating a Custom Filter

Now, let's create a custom OncePerRequestFilter. This filter will:

  • Intercept every incoming HTTP request.
  • Extract a client identifier (e.g., remote IP address).
  • Use our RateLimiterService to check if the request is allowed.
  • If not allowed, it will send an HTTP 429 (Too Many Requests) response.

This ensures the rate limit is enforced early in the request lifecycle.

Filter Code Example

Here's the full code for our custom RateLimitingFilter. Note how it injects the RateLimiterService and handles the response.

We'll manually add this filter to our Spring Security chain later.

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

public class RateLimitingFilter extends OncePerRequestFilter {

    private final RateLimiterService rateLimiterService;

    public RateLimitingFilter(RateLimiterService rateLimiterService) {
        this.rateLimiterService = rateLimiterService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String clientIp = request.getRemoteAddr(); // Simple IP-based identifier

        if (!rateLimiterService.tryAcquire(clientIp)) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("Too many requests from this IP. Please try again later.");
            return; // Stop the filter chain
        }

        filterChain.doFilter(request, response);
    }

    public static void main(String[] args) throws ServletException, IOException {
        // Simulate filter behavior for demo
        RateLimiterService limiter = new RateLimiterService(2, 5000); // 2 requests per 5 seconds
        RateLimitingFilter filter = new RateLimitingFilter(limiter);

        // Mock HttpServletRequest and HttpServletResponse for testing
        HttpServletRequest mockRequest = new MockHttpServletRequest("127.0.0.1");
        HttpServletResponse mockResponse = new MockHttpServletResponse();
        FilterChain mockChain = (req, res) -> System.out.println("Request processed by chain.");

        System.out.println("Attempt 1:");
        filter.doFilterInternal(mockRequest, mockResponse, mockChain);
        System.out.println("Response status: " + mockResponse.getStatus());

        System.out.println("Attempt 2:");
        mockResponse = new MockHttpServletResponse(); // Reset response for next attempt
        filter.doFilterInternal(mockRequest, mockResponse, mockChain);
        System.out.println("Response status: " + mockResponse.getStatus());

        System.out.println("Attempt 3 (should be blocked): ");
        mockResponse = new MockHttpServletResponse();
        filter.doFilterInternal(mockRequest, mockResponse, mockChain);
        System.out.println("Response status: " + mockResponse.getStatus());
    }

    // Simple mock classes for runnable example
    private static class MockHttpServletRequest implements HttpServletRequest {
        private String remoteAddr;
        public MockHttpServletRequest(String remoteAddr) { this.remoteAddr = remoteAddr; }
        @Override public String getRemoteAddr() { return remoteAddr; }
        @Override public String getMethod() { return "GET"; }
        @Override public String getRequestURI() { return "/test"; }
        @Override public String getHeader(String name) { return null; }
        @Override public java.util.Enumeration<String> getHeaderNames() { return null; }
        @Override public java.util.Enumeration<String> getHeaders(String name) { return null; }
        @Override public int getIntHeader(String name) { return -1; }
        @Override public String getContextPath() { return ""; }
        @Override public java.security.Principal getUserPrincipal() { return null; }
        @Override public boolean isUserInRole(String role) { return false; }
        @Override public String getAuthType() { return null; }
        @Override public String getRemoteUser() { return null; }
        @Override public String getPathInfo() { return null; }
        @Override public String getPathTranslated() { return null; }
        @Override public String getQueryString() { return null; }
        @Override public String getRemoteHost() { return null; }
        @Override public int getRemotePort() { return 0; }
        @Override public String getLocalAddr() { return null; }
        @Override public String getLocalName() { return null; }
        @Override public int getLocalPort() { return 0; }
        @Override public java.util.Locale getLocale() { return null; }
        @Override public java.util.Enumeration<java.util.Locale> getLocales() { return null; }
        @Override public String getScheme() { return "http"; }
        @Override public String getServerName() { return "localhost"; }
        @Override public int getServerPort() { return 8080; }
        @Override public java.io.BufferedReader getReader() throws IOException { return null; }
        @Override public String getRealPath(String path) { return null; }
        @Override public long getDateHeader(String name) { return -1; }
        @Override public java.io.InputStream getInputStream() throws IOException { return null; }
        @Override public int getContentLength() { return -1; }
        @Override public long getContentLengthLong() { return -1; }
        @Override public String getContentType() { return null; }
        @Override public String getCharacterEncoding() { return null; }
        @Override public void setCharacterEncoding(String env) throws java.io.UnsupportedEncodingException {}
        @Override public java.util.Map<String, String[]> getParameterMap() { return null; }
        @Override public String getParameter(String name) { return null; }
        @Override public java.util.Enumeration<String> getParameterNames() { return null; }
        @Override public String[] getParameterValues(String name) { return null; }
        @Override public String getProtocol() { return "HTTP/1.1"; }
        @Override public String getServletPath() { return ""; }
        @Override public jakarta.servlet.http.HttpSession getSession(boolean create) { return null; }
        @Override public jakarta.servlet.http.HttpSession getSession() { return null; }
        @Override public String getRequestedSessionId() { return null; }
        @Override public boolean isRequestedSessionIdValid() { return false; }
        @Override public boolean isRequestedSessionIdFromCookie() { return false; }
        @Override public boolean isRequestedSessionIdFromURL() { return false; }
        @Override public boolean isRequestedSessionIdFromUrl() { return false; }
        @Override public jakarta.servlet.RequestDispatcher getRequestDispatcher(String path) { return null; }
        @Override public String getForwardURI() { return null; }
        @Override public String getForwardQueryString() { return null; }
        @Override public String getForwardContextPath() { return null; }
        @Override public String getForwardServletPath() { return null; }
        @Override public String getForwardPathInfo() { return null; }
        @Override public String getForwardRequestURI() { return null; }
        @Override public String getDispatcherType() { return null; }
        @Override public Object getAttribute(String name) { return null; }
        @Override public java.util.Enumeration<String> getAttributeNames() { return null; }
        @Override public void setAttribute(String name, Object o) {}
        @Override public void removeAttribute(String name) {}
        @Override public jakarta.servlet.ServletContext getServletContext() { return null; }
        @Override public boolean isAsyncStarted() { return false; }
        @Override public boolean isAsyncSupported() { return false; }
        @Override public jakarta.servlet.AsyncContext startAsync() throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext startAsync(jakarta.servlet.ServletRequest servletRequest, jakarta.servlet.ServletResponse servletResponse) throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext getAsyncContext() { return null; }
    }

    private static class MockHttpServletResponse implements HttpServletResponse {
        private int status = SC_OK;
        private StringWriter writer = new StringWriter();
        private PrintWriter printWriter = new PrintWriter(writer);

        @Override public void setStatus(int sc) { this.status = sc; }
        @Override public int getStatus() { return status; }
        @Override public PrintWriter getWriter() throws IOException { return printWriter; }
        @Override public String toString() { return "Status: " + status + ", Body: " + writer.toString(); }
        @Override public void addCookie(jakarta.servlet.http.Cookie cookie) {}
        @Override public boolean containsHeader(String name) { return false; }
        @Override public String encodeURL(String url) { return url; }
        @Override public String encodeRedirectURL(String url) { return url; }
        @Override public String encodeUrl(String url) { return url; }
        @Override public String encodeRedirectUrl(String url) { return url; }
        @Override public void sendError(int sc, String msg) throws IOException {}
        @Override public void sendError(int sc) throws IOException {}
        @Override public void sendRedirect(String location) throws IOException {}
        @Override public void setDateHeader(String name, long date) {}
        @Override public void addDateHeader(String name, long date) {}
        @Override public void setHeader(String name, String value) {}
        @Override public void addHeader(String name, String value) {}
        @Override public void setIntHeader(String name, int value) {}
        @Override public void addIntHeader(String name, int value) {}
        @Override public void setContentType(String type) {}
        @Override public String getContentType() { return null; }
        @Override public void setCharacterEncoding(String charset) {}
        @Override public String getCharacterEncoding() { return null; }
        @Override public jakarta.servlet.ServletOutputStream getOutputStream() throws IOException { return null; }
        @Override public int getBufferSize() { return 0; }
        @Override public void setBufferSize(int size) {}
        @Override public void flushBuffer() throws IOException {}
        @Override public void resetBuffer() {}
        @Override public boolean isCommitted() { return false; }
        @Override public void reset() {}
        @Override public void setLocale(java.util.Locale loc) {}
        @Override public java.util.Locale getLocale() { return null; }
        @Override public void setContentLength(int len) {}
        @Override public void setContentLengthLong(long len) {}
        @Override public int getHeader(String name) { return -1; }
        @Override public java.util.Collection<String> getHeaders(String name) { return null; }
        @Override public java.util.Collection<String> getHeaderNames() { return null; }
        @Override public String getTrailerFields() { return null; }
        @Override public void setTrailerFields(String trailerFields) {}
        @Override public boolean isAsyncSupported() { return false; }
        @Override public jakarta.servlet.AsyncContext startAsync() throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext startAsync(jakarta.servlet.ServletRequest servletRequest, jakarta.servlet.ServletResponse servletResponse) throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext getAsyncContext() { return null; }
    }
}

Registering the Filter

To make our RateLimitingFilter active, we need to register it within Spring Security's configuration.

We typically do this in our SecurityFilterChain bean, ensuring it runs before other security filters, especially for unauthenticated endpoints like login.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    private final RateLimiterService rateLimiterService;

    public SecurityConfig(RateLimiterService rateLimiterService) {
        this.rateLimiterService = rateLimiterService;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Disable CSRF for simplicity in API examples
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/login").permitAll() // Allow login without auth
                .requestMatchers("/api/public/**").permitAll() // Public endpoints
                .anyRequest().authenticated() // All other requests need auth
            )
            // Add our custom rate limiting filter
            .addFilterBefore(new RateLimitingFilter(rateLimiterService),
                             UsernamePasswordAuthenticationFilter.class); // Or other relevant filter

        return http.build();
    }

    @Bean
    public RateLimiterService rateLimiterService() {
        // Configure max 5 requests per 60 seconds
        return new RateLimiterService(5, 60000);
    }

    public static void main(String[] args) {
        System.out.println("Spring Security configuration example.");
        System.out.println("RateLimiterService and RateLimitingFilter would be integrated here.");
    }
}

Example with a Controller

Let's imagine a simple login endpoint. With our filter in place, repeated requests from the same IP will be blocked after the limit is reached.

This is a simplified controller, but it shows where the filter would act before the actual login logic.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;

// These classes (RateLimiterService, RateLimitingFilter) would be defined as @Component/@Configuration beans
// in a full Spring Boot application, as shown in previous scenes.

@RestController
class MyController {

    @GetMapping("/api/login")
    public ResponseEntity<String> login() {
        return ResponseEntity.ok("Login successful (simulated)!");
    }

    @GetMapping("/api/public/hello")
    public ResponseEntity<String> publicHello() {
        return ResponseEntity.ok("Hello from public endpoint!");
    }
}

// A simplified Spring Boot application to make it conceptually runnable
@SpringBootApplication
public class RateLimitApp {

    // The beans for RateLimiterService and RateLimitingFilter
    // would typically be defined in SecurityConfig or another @Configuration class.
    // We include them here for a self-contained runnable demonstration context.
    @Bean
    public RateLimiterService rateLimiterService() {
        return new RateLimiterService(3, 10000); // 3 requests per 10 seconds
    }

    @Bean
    public RateLimitingFilter rateLimitingFilter(RateLimiterService service) {
        return new RateLimitingFilter(service);
    }

    public static void main(String[] args) {
        System.out.println("This demonstrates a Spring Boot app structure.");
        System.out.println("The RateLimitingFilter would protect /api/login.");
        System.out.println("In a running app, repeated access to /api/login from the same IP");
        System.out.println("would result in HTTP 429 after 3 requests within 10 seconds.");
        // To actually run this, you'd uncomment:
        // SpringApplication.run(RateLimitApp.class, args);
    }
}

Testing Your Rate Limiter

Once configured, you can test your rate limiter by sending multiple requests to the protected endpoint (e.g., /api/login) from the same IP address.

You should observe the following:

  • Initial requests succeed (HTTP 200 OK).
  • After exceeding the limit, subsequent requests fail with an HTTP 429 (Too Many Requests) status code.

This confirms your rate limiter is actively protecting your API.

Quick Check

Rate limiting is an essential security practice. Which of the following are primary benefits of implementing rate limiting on API endpoints?

Recap: Rate Limiting

You've learned about the importance of rate limiting to protect your API endpoints from attacks like brute-force and DoS.

We explored how to implement a basic fixed-window rate limiter using a custom OncePerRequestFilter and a dedicated service in a Spring Boot application.

This fundamental security measure helps ensure your application's stability and resource availability.

Frequently asked questions

Is the “Rate Limiting API Access” lesson free?

Yes — the full text of “Rate Limiting API Access” is free to read here on the web, and the Spring Security 6 & JWT Authentication course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Spring Security 6 & JWT Authentication course, upgrade to CoddyKit PRO.

What will I learn in “Rate Limiting API Access”?

Protect your authentication endpoints and APIs from brute-force and denial-of-service attacks using rate limiting. You practise Spring Security 6 & JWT Authentication with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Spring Security 6 & JWT Authentication?

No prior experience is required. Spring Security 6 & JWT Authentication on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Rate Limiting API Access” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Spring Security 6 & JWT Authentication lesson?

Yes. Every Spring Security 6 & JWT Authentication lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Implementing Multi-Factor Authentication
  2. Rate Limiting API Access
  3. Custom Authentication Event Handling
  4. Account Lockout and Brute-Force Protection
← Back to Spring Security 6 & JWT Authentication