
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/productsGET /api/products/{id}GET /actuator/health
This first test checks baseline read performance and service health.
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.
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.
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
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-sizespring.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:
401or403if authentication is misconfigured429if rate limiting kicks in500if the application fails internally502or504if 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/productsmay stay fast due to caching/api/orders/checkout/{cartId}may degrade under write-heavy traffic/api/auth/loginmay 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.
LoadForge Team
LoadForge is a load and performance testing platform built on Locust. Our team has been shipping load tests against production systems since 2018, and we write these guides from real customer engagements.
Related guides
Keep going with more guides from the same category.

ASP.NET Load Testing Guide with LoadForge
Learn how to load test ASP.NET applications with LoadForge to find performance issues and ensure your app handles peak traffic.

CakePHP Load Testing Guide with LoadForge
Load test CakePHP applications with LoadForge to benchmark app performance, simulate traffic, and improve scalability.

Django Load Testing Guide with LoadForge
Discover how to load test Django applications with LoadForge to measure performance, handle traffic spikes, and improve stability.