LoadForge LogoLoadForge

Spring Boot Load Testing Guide with LoadForge

Spring Boot Load Testing Guide with LoadForge

Introduction

Spring Boot is one of the most popular frameworks for building Java web applications, REST APIs, and microservices. Its auto-configuration, embedded servers, and production-ready ecosystem make it a strong choice for teams that need to move fast. But even well-designed Spring Boot services can struggle under real-world traffic if they are not properly load tested.

A Spring Boot load testing strategy helps you understand how your application behaves under normal traffic, peak demand, and stress conditions. Whether you are serving REST endpoints, handling authentication with Spring Security, or processing database-heavy business logic, performance testing is essential to measure throughput, reduce latency, and prevent failures before they impact users.

In this guide, you will learn how to load test Spring Boot applications with LoadForge using realistic Locust scripts. We will cover basic endpoint testing, authenticated API flows, e-commerce style transaction scenarios, and file upload testing. Along the way, we will also look at how to interpret results and optimize common Spring Boot bottlenecks. Because LoadForge runs on cloud-based infrastructure with distributed testing, real-time reporting, global test locations, and CI/CD integration, it is well suited for testing Spring Boot applications at scale.

Prerequisites

Before you start load testing your Spring Boot application, make sure you have the following:

  • A running Spring Boot application in a test or staging environment
  • The base URL for your application, such as https://staging-api.example.com
  • Knowledge of the main endpoints you want to test
  • Test user accounts for authentication flows
  • Representative test data, such as product IDs, order payloads, or uploaded files
  • An understanding of expected traffic patterns
  • A LoadForge account to run distributed load tests with Locust scripts

It also helps to know which Spring Boot components are involved in request handling, such as:

  • Spring MVC or Spring WebFlux
  • Spring Security
  • Hibernate / JPA
  • HikariCP connection pool
  • Caching layers like Redis
  • External dependencies such as payment gateways or message brokers

For realistic performance testing, use a non-production environment that closely matches production in terms of application configuration, database size, JVM settings, and infrastructure.

Understanding Spring Boot Under Load

Spring Boot applications often perform well in development and then hit bottlenecks when concurrency increases. Load testing helps expose those issues before they become outages.

How Spring Boot handles requests

In a typical Spring Boot application using Spring MVC, incoming HTTP requests are handled by a servlet container such as:

  • Tomcat
  • Jetty
  • Undertow

Each request consumes a worker thread. If traffic spikes and thread pools are exhausted, response times increase and requests may queue or fail.

If your application uses Spring WebFlux, the concurrency model is different and can handle high numbers of connections more efficiently, but downstream blocking dependencies like JDBC calls can still create bottlenecks.

Common Spring Boot performance bottlenecks

When load testing Spring Boot, the most common bottlenecks include:

Thread pool exhaustion

Tomcat’s request handling threads can become saturated under heavy traffic, especially when endpoints perform slow database or remote service calls.

Database connection pool limits

Spring Boot commonly uses HikariCP. If the connection pool is too small, requests may wait for a connection, increasing latency and lowering throughput.

Inefficient JPA queries

Hibernate can introduce performance issues through:

  • N+1 query problems
  • Missing indexes
  • Over-fetching entities
  • Slow joins
  • Excessive lazy loading

These problems often appear only under concurrent load.

Authentication overhead

Spring Security with JWT, OAuth2, or session-based login can add measurable latency, especially if token validation or user lookup hits a database or external identity provider.

Serialization and payload size

Large JSON payloads, expensive object mapping, and excessive response data can increase CPU and memory usage.

JVM garbage collection pressure

High request volume may create object allocation spikes, leading to garbage collection pauses and tail latency problems.

Downstream service dependencies

Even if your Spring Boot app is fast, calls to Redis, Kafka, Elasticsearch, payment providers, or internal microservices may slow overall response time.

A good Spring Boot load test should simulate realistic user behavior, not just hammer a single endpoint. That is where Locust and LoadForge are especially useful.

Writing Your First Load Test

Let’s start with a basic load testing script for a simple Spring Boot REST API. Imagine your application exposes product catalog endpoints like:

  • GET /api/products
  • GET /api/products/{id}
  • GET /actuator/health

This first test checks baseline read performance and service health.

python
from locust import HttpUser, task, between
 
class SpringBootCatalogUser(HttpUser):
    wait_time = between(1, 3)
 
    def on_start(self):
        self.product_ids = [101, 102, 103, 104, 105]
 
    @task(5)
    def list_products(self):
        params = {
            "page": 0,
            "size": 20,
            "sort": "name,asc",
            "category": "electronics"
        }
        with self.client.get(
            "/api/products",
            params=params,
            name="GET /api/products",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"Unexpected status code: {response.status_code}")
                return
 
            try:
                data = response.json()
                if "content" not in data:
                    response.failure("Missing 'content' field in product list response")
            except Exception as e:
                response.failure(f"Invalid JSON response: {e}")
 
    @task(3)
    def get_product_details(self):
        product_id = self.product_ids[self.environment.runner.user_count % len(self.product_ids)]
        with self.client.get(
            f"/api/products/{product_id}",
            name="GET /api/products/:id",
            catch_response=True
        ) as response:
            if response.status_code == 200:
                product = response.json()
                if "id" not in product or "price" not in product:
                    response.failure("Product response missing required fields")
            else:
                response.failure(f"Failed to fetch product {product_id}")
 
    @task(1)
    def health_check(self):
        self.client.get("/actuator/health", name="GET /actuator/health")

What this script does

This Locust test simulates users browsing a Spring Boot product API. It:

  • Lists products with pagination and filtering
  • Fetches individual product details
  • Checks the health endpoint occasionally

This is a good starting point for measuring:

  • Baseline response times
  • Throughput for common read endpoints
  • Error rates under moderate concurrent traffic

Why this matters for Spring Boot

Read-heavy APIs are often the first thing teams test, but even simple GET endpoints can reveal hidden issues in:

  • Hibernate query performance
  • JSON serialization
  • Cache misses
  • Thread pool saturation

When you run this script in LoadForge, you can scale to thousands of concurrent users from multiple regions and use real-time reporting to identify latency spikes quickly.

Advanced Load Testing Scenarios

A basic endpoint test is useful, but real Spring Boot applications usually involve authentication, stateful workflows, writes, and file handling. Below are several more realistic performance testing scenarios.

Authenticated API testing with Spring Security and JWT

Many Spring Boot applications use Spring Security with JWT-based login. In this example, users authenticate at /api/auth/login, receive a bearer token, and then call secured endpoints.

python
from locust import HttpUser, task, between
import random
 
class SpringBootAuthenticatedUser(HttpUser):
    wait_time = between(1, 2)
 
    def on_start(self):
        credentials = random.choice([
            {"email": "qa.user1@example.com", "password": "Password123!"},
            {"email": "qa.user2@example.com", "password": "Password123!"},
            {"email": "qa.user3@example.com", "password": "Password123!"}
        ])
 
        with self.client.post(
            "/api/auth/login",
            json=credentials,
            name="POST /api/auth/login",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"Login failed: {response.status_code}")
                return
 
            body = response.json()
            token = body.get("accessToken")
            if not token:
                response.failure("No accessToken in login response")
                return
 
            self.client.headers.update({
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json"
            })
 
    @task(4)
    def get_profile(self):
        with self.client.get(
            "/api/users/me",
            name="GET /api/users/me",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"Profile request failed: {response.status_code}")
 
    @task(3)
    def list_orders(self):
        with self.client.get(
            "/api/orders?status=OPEN&page=0&size=10",
            name="GET /api/orders",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"Orders request failed: {response.status_code}")
 
    @task(2)
    def refresh_token(self):
        with self.client.post(
            "/api/auth/refresh",
            json={"refreshToken": "static-test-refresh-token"},
            name="POST /api/auth/refresh",
            catch_response=True
        ) as response:
            if response.status_code not in [200, 401]:
                response.failure(f"Unexpected refresh token response: {response.status_code}")

What this test reveals

This script is useful for measuring:

  • Authentication latency
  • Overhead introduced by Spring Security filters
  • Token validation cost
  • Performance of secured endpoints under concurrency

If login is slow, investigate:

  • Password hashing settings
  • User lookup queries
  • External identity provider latency
  • JWT signing and verification overhead

End-to-end transaction flow for a Spring Boot e-commerce API

Single-endpoint testing does not reflect real user behavior. A better performance testing approach is to simulate a multi-step user journey. In this example, users browse products, add items to a cart, and place an order.

python
from locust import HttpUser, task, between
import random
 
class SpringBootEcommerceUser(HttpUser):
    wait_time = between(2, 5)
 
    def on_start(self):
        self.client.headers.update({"Content-Type": "application/json"})
        self.product_ids = [2001, 2002, 2003, 2004, 2005]
        self.cart_id = None
 
        login_payload = {
            "email": f"shopper{random.randint(1, 50)}@example.com",
            "password": "Password123!"
        }
 
        response = self.client.post("/api/auth/login", json=login_payload, name="POST /api/auth/login")
        if response.status_code == 200:
            token = response.json().get("accessToken")
            if token:
                self.client.headers.update({"Authorization": f"Bearer {token}"})
 
    @task(5)
    def browse_catalog(self):
        category = random.choice(["electronics", "books", "home", "fitness"])
        self.client.get(
            f"/api/products?category={category}&page=0&size=12&sort=popularity,desc",
            name="GET /api/products?category"
        )
 
    @task(4)
    def view_product(self):
        product_id = random.choice(self.product_ids)
        self.client.get(f"/api/products/{product_id}", name="GET /api/products/:id")
 
    @task(2)
    def add_to_cart(self):
        if not self.cart_id:
            response = self.client.post("/api/cart", json={}, name="POST /api/cart")
            if response.status_code == 201:
                self.cart_id = response.json().get("id")
 
        if self.cart_id:
            payload = {
                "productId": random.choice(self.product_ids),
                "quantity": random.randint(1, 3)
            }
            self.client.post(
                f"/api/cart/{self.cart_id}/items",
                json=payload,
                name="POST /api/cart/:id/items"
            )
 
    @task(1)
    def checkout(self):
        if not self.cart_id:
            return
 
        payload = {
            "shippingAddress": {
                "fullName": "Test Customer",
                "line1": "123 Main Street",
                "city": "Austin",
                "state": "TX",
                "postalCode": "78701",
                "country": "US"
            },
            "paymentMethod": {
                "type": "CREDIT_CARD",
                "token": "tok_visa_test_4242"
            },
            "deliveryMethod": "STANDARD"
        }
 
        with self.client.post(
            f"/api/orders/checkout/{self.cart_id}",
            json=payload,
            name="POST /api/orders/checkout/:cartId",
            catch_response=True
        ) as response:
            if response.status_code not in [200, 201]:
                response.failure(f"Checkout failed: {response.status_code}")

Why this scenario matters

This is a far more realistic Spring Boot load test because it exercises:

  • Authentication
  • Product search and detail retrieval
  • Cart creation and updates
  • Checkout logic
  • Database writes and transactional consistency

This kind of scenario often exposes bottlenecks in:

  • Transaction management
  • Inventory checks
  • Payment service integration
  • Database locking
  • Cart session handling
  • Order creation workflows

LoadForge is especially useful here because distributed testing allows you to model real customer traffic from multiple regions and compare how transaction latency changes as concurrency increases.

File upload testing for Spring Boot multipart endpoints

Spring Boot applications often support file uploads for profile images, documents, or bulk imports. These endpoints behave very differently under load because they consume more memory, CPU, disk I/O, and network bandwidth.

Suppose your app exposes a secured file upload endpoint:

  • POST /api/documents/upload
python
from locust import HttpUser, task, between
from io import BytesIO
import random
 
class SpringBootFileUploadUser(HttpUser):
    wait_time = between(3, 6)
 
    def on_start(self):
        login_payload = {
            "email": "uploader@example.com",
            "password": "Password123!"
        }
        response = self.client.post("/api/auth/login", json=login_payload, name="POST /api/auth/login")
        if response.status_code == 200:
            token = response.json().get("accessToken")
            if token:
                self.client.headers.update({"Authorization": f"Bearer {token}"})
 
    @task
    def upload_document(self):
        file_size_kb = random.choice([100, 500, 1024])
        file_content = BytesIO(b"x" * 1024 * file_size_kb)
 
        files = {
            "file": (
                f"report-{file_size_kb}kb.pdf",
                file_content,
                "application/pdf"
            )
        }
 
        data = {
            "documentType": "INVOICE",
            "customerId": "CUST-100045",
            "description": "Monthly invoice upload for account reconciliation"
        }
 
        with self.client.post(
            "/api/documents/upload",
            files=files,
            data=data,
            name="POST /api/documents/upload",
            catch_response=True
        ) as response:
            if response.status_code != 201:
                response.failure(f"Upload failed: {response.status_code}")

What to watch for in file upload tests

Multipart upload performance testing can reveal issues with:

  • Request size limits
  • Temporary file storage
  • Reverse proxy buffering
  • Heap pressure
  • Virus scanning or post-processing delays
  • Object storage integration latency

For Spring Boot specifically, review settings like:

  • spring.servlet.multipart.max-file-size
  • spring.servlet.multipart.max-request-size
  • Tomcat connector limits
  • Nginx or API gateway body size restrictions

Analyzing Your Results

Once your Spring Boot load testing scripts are running in LoadForge, the next step is interpreting the results correctly.

Key metrics to review

Response time percentiles

Do not focus only on average response time. For Spring Boot performance testing, percentiles are more important:

  • P50 shows typical user experience
  • P95 shows how the system performs for slower requests
  • P99 reveals tail latency and often highlights contention or garbage collection issues

A healthy application may have a low average but still suffer from poor P95 or P99 latency.

Requests per second

This measures throughput. If response times rise sharply while requests per second plateau, you may be hitting a bottleneck such as:

  • Thread pool saturation
  • Database pool exhaustion
  • CPU limits
  • Lock contention

Error rate

Watch for:

  • 401 or 403 if authentication is misconfigured
  • 429 if rate limiting kicks in
  • 500 if the application fails internally
  • 502 or 504 if proxies or gateways time out

In Spring Boot apps, bursts of 500 errors during stress testing often point to downstream dependency failures or unhandled concurrency issues.

Endpoint-level breakdown

Compare endpoint performance individually. For example:

  • /api/products may stay fast due to caching
  • /api/orders/checkout/{cartId} may degrade under write-heavy traffic
  • /api/auth/login may become slow due to password hashing or identity provider calls

LoadForge’s real-time reporting helps you spot these endpoint-specific patterns quickly.

Correlate with application metrics

For the best Spring Boot load testing results, compare LoadForge metrics with application-side metrics from:

  • Spring Boot Actuator
  • Micrometer
  • Prometheus
  • Grafana
  • APM tools like New Relic, Datadog, or Dynatrace

Pay special attention to:

  • JVM heap and garbage collection
  • Tomcat thread usage
  • HikariCP active and pending connections
  • Database query latency
  • CPU and memory utilization
  • Cache hit ratio

Ramp-up behavior

A system that handles 100 users may fail at 500 because of nonlinear bottlenecks. Use LoadForge to ramp traffic gradually and identify the exact point where latency, errors, or throughput begin to degrade.

Performance Optimization Tips

After load testing your Spring Boot application, you will usually find one or more optimization opportunities.

Tune connection pools carefully

If your database pool is too small, requests wait. If it is too large, the database can become overloaded. Tune HikariCP based on actual database capacity and concurrent request patterns.

Optimize slow JPA queries

Use SQL logs, query analysis, and database execution plans to identify:

  • N+1 issues
  • Missing indexes
  • Large result sets
  • Inefficient joins
  • Unnecessary entity loading

For high-traffic endpoints, consider DTO projections or native queries where appropriate.

Add caching for read-heavy endpoints

If product listings, configuration data, or profile lookups are frequently requested, caching can reduce database load significantly.

Review Spring Security overhead

If authentication endpoints are slow:

  • Cache user details if safe
  • Reduce unnecessary database lookups
  • Validate JWTs efficiently
  • Offload identity operations where possible

Tune embedded server threads

Tomcat, Jetty, or Undertow thread settings should align with your workload and infrastructure. More threads are not always better, especially if the real bottleneck is the database.

Reduce payload sizes

Large JSON responses increase serialization cost and bandwidth use. Return only necessary fields and use pagination consistently.

Monitor garbage collection

If P99 latency spikes under heavy load, inspect JVM garbage collection behavior. High allocation rates and oversized heaps can both create performance problems.

Test with production-like data

Small test datasets can hide performance issues. Spring Boot applications often behave very differently when tables are large and indexes are under real pressure.

Common Pitfalls to Avoid

Load testing Spring Boot applications is straightforward in principle, but teams often make mistakes that lead to misleading results.

Testing only the health endpoint

/actuator/health is useful for monitoring but tells you almost nothing about real application performance. Focus on business-critical endpoints.

Ignoring authentication flows

If your real users authenticate, your load test should include Spring Security and token handling. Otherwise, you may miss a major source of latency.

Using unrealistic traffic patterns

A test that sends identical requests to one endpoint does not reflect actual user behavior. Mix reads, writes, searches, and transactions.

Not validating responses

A fast 500 error is not success. Use catch_response=True in Locust to confirm the application returns correct responses.

Overlooking downstream dependencies

Your Spring Boot app may depend on databases, caches, queues, search engines, and third-party APIs. A realistic performance test should account for these dependencies.

Running tests against underpowered staging environments

If the test environment does not resemble production, the results may not be useful. Try to match infrastructure and configuration as closely as possible.

Failing to ramp gradually

Jumping immediately to peak traffic can make it harder to identify the actual breaking point. Use staged ramp-up patterns.

Not correlating app-side metrics

Load testing data alone is incomplete. Always compare response times and throughput with JVM, database, and infrastructure metrics.

Conclusion

Spring Boot is a powerful framework, but every application has limits. Load testing helps you find those limits before your users do. By testing realistic scenarios such as catalog browsing, JWT authentication, checkout flows, and file uploads, you can measure throughput, reduce latency, and prevent failures in your Spring Boot services.

With LoadForge, you can run Spring Boot load testing at scale using Locust-based scripts, distributed testing, real-time reporting, cloud-based infrastructure, global test locations, and CI/CD integration. That makes it easier to validate performance before releases and catch bottlenecks early.

If you are ready to improve the reliability and scalability of your Spring Boot application, try LoadForge and start building realistic performance tests today.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.