Spring Boot & Tomcat Tuning for High Concurrency: 17 Techniques & 12 Key Parameters
This guide walks through a complete Spring Boot and Tomcat performance tuning workflow for high‑traffic services, covering thread‑model fundamentals, capacity planning, 17 practical engineering tricks, 12 essential Tomcat parameters, and strategies for graceful deployment, observability, and fault isolation.
Why Spring Boot Services Fail Under High Load
Typical symptoms observed in production include rising response times without full CPU utilization, Tomcat thread‑pool saturation, request queuing, database‑pool exhaustion, brief failures during rolling upgrades, and misleading benchmark results that disappear under real traffic spikes.
The root cause is rarely a single configuration value. Instead, an end‑to‑end request chain is not designed holistically:
Client → LB/Gateway → Tomcat Connector → Tomcat Worker Thread → Filter/Interceptor → Controller → Service → DB/Redis/RPC/MQ/Third‑party → Response Write → Connection Reuse/CloseAny blocking point in this chain amplifies upstream pressure, leading to thread starvation, request queuing, time‑outs, retry storms, and cascade failures.
The tuning goal is therefore four‑fold: shorten request residence time, isolate blocking resources, enforce overload boundaries, and make deployment, scaling, and recovery predictable.
Understanding Tomcat’s Core Thread Model
In the default NIO mode a request passes through three main components: Acceptor: Listens on the port and accepts TCP connections. Poller: Uses a Selector to watch for read/write readiness. Executor (or Worker thread): Executes the servlet chain and business code.
The simplified flow is:
TCP connection → Acceptor accepts → Poller detects readable event → Worker reads request, runs business logic, writes response → Keep‑Alive keeps or closes the connectionKey insight: Tomcat’s NIO only reduces thread waste at the network I/O level; once the request reaches the servlet layer it is still processed by a regular Java thread. Any of the following will occupy that thread for a long time:
Blocking JDBC queries
Blocking Redis/RPC calls
Synchronous file I/O, logging, large object serialization
Lock contention or long transactions
Thus the real bottleneck is not “cannot accept connections” but “worker threads are held for too long”.
How Spring Boot Auto‑Configures Embedded Tomcat
When the spring-boot-starter-web dependency is present, Boot creates a TomcatServletWebServerFactory which in turn builds an embedded TomcatWebServer. Default connector values are:
maxThreads = 200 minSpareThreads = 10 acceptCount = 100 maxConnections = 10000These defaults are fine for development but usually insufficient for production because:
200 threads may not handle long‑running business logic during spikes. acceptCount=100 is too conservative for burst traffic. maxConnections=10000 can be quickly consumed by many idle Keep‑Alive connections.
If downstream pools are smaller, a larger Tomcat thread pool can actually increase collective blocking.
Typical Misconfiguration Example
Consider the following settings:
Tomcat maxThreads=400 HikariCP maximumPoolSize=20 Each request performs one DB query
DB query latency spikes from 20 ms to 800 ms
What happens:
400 concurrent requests enter Tomcat.
Only 20 threads can acquire a DB connection; the rest block on the pool.
Blocked threads keep the connection open, causing the request queue to grow.
Clients time out, the gateway retries, creating a secondary traffic surge.
This demonstrates why tuning must consider the whole chain rather than a single component.
Full‑Link Architecture for Production‑Grade Services
A recommended skeleton looks like:
+----------------------+
| CDN / WAF / SLB |
+----------+-----------+
|
+-------+------+
| API Gateway |
+------+------+
|
+------+------+
| Spring Boot |
| Tomcat Pod |
+------+------+
|
+------+------------------+--------------+------+
| | | | |
Redis Cache MQ/Kafka RPC/Feign DB/TiDB Third‑party APIA stable architecture should provide six capabilities:
Entrance rate limiting to protect the container.
Thread isolation to keep slow tasks out of Tomcat workers.
Coordinated connection‑pool sizing across Tomcat, DB, Redis, HTTP clients.
Circuit breaking for downstream jitter.
Asynchronous spike reduction via MQ.
Graceful start/stop without request loss.
Capacity Planning Before Parameter Tweaking
Many performance problems stem from missing capacity estimation. Using Little’s Law you can roughly calculate required concurrent threads:
Concurrent ≈ Throughput (QPS) × Avg‑Response‑Time (seconds)Example: target 1500 QPS with 120 ms average latency → 1500 × 0.12 = 180 concurrent requests. This does not directly become the final maxThreads value but indicates that the worker pool should not be far below 180, and downstream pools must be sized accordingly.
Guidelines:
CPU‑bound services: maxThreads ≈ CPU‑cores × 2‑4 I/O‑bound services: higher thread counts are possible but must be paired with matching downstream pool sizes and timeout policies.
Never let DB pool size be smaller than the expected concurrent load; otherwise the whole system stalls.
Four Groups of Parameters to Tune Together
Tomcat maxThreads Tomcat acceptCount / maxConnections Database connection‑pool size
HTTP client pool + timeout settings
Practical recommendations:
CPU‑intensive services: keep thread count modest (CPU × 2‑4).
I/O‑intensive services: higher thread counts are acceptable but must be coordinated with downstream limits.
Database pool sizing should be driven by the DB’s capacity, not by Tomcat’s thread count.
External HTTP client pools must not allow unlimited parallel calls.
Load‑Testing Must Reflect Real Production Characteristics
Key aspects to include:
Keep‑Alive usage ratio
Hot‑spot API proportion
Mixed request sizes
Cache hit/miss scenarios
Injected slow SQL / RPC calls
Release‑time traffic shaping and scaling events
Retry storms or sudden spikes
Otherwise you only obtain “lab scores” that do not translate to production reliability.
17 Practical Spring Boot Engineering Techniques
Externalise all critical concurrency parameters – keep them out of code and manage via configuration files or a config centre.
server:
port: 8080
shutdown: graceful
tomcat:
threads:
max: 300
min-spare: 30
accept-count: 800
max-connections: 12000
connection-timeout: 3000
keep-alive-timeout: 15000
max-keep-alive-requests: 500
max-http-form-post-size: 2MB
max-swallow-size: 4MB
accesslog:
enabled: false
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
datasource:
hikari:
maximum-pool-size: 40
minimum-idle: 10
connection-timeout: 2000
validation-timeout: 1000
idle-timeout: 300000
max-lifetime: 1200000
leak-detection-threshold: 10000
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metricsCustomize Tomcat via TomcatServletWebServerFactory
package com.example.demo.config;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class TomcatTuningCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractHttp11Protocol<?> protocol) {
protocol.setMaxThreads(300);
protocol.setMinSpareThreads(30);
protocol.setAcceptCount(800);
protocol.setMaxConnections(12000);
protocol.setConnectionTimeout(3000);
protocol.setKeepAliveTimeout(15000);
protocol.setMaxKeepAliveRequests(500);
protocol.setMaxHttpHeaderSize(16 * 1024);
}
});
}
}Offload time‑consuming work from Tomcat workers – use a dedicated async executor.
package com.example.demo.config;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncExecutorConfig {
@Bean(name = "bizExecutor")
public Executor bizExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(32);
executor.setMaxPoolSize(128);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("biz-exec-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}Isolate thread pools per task type – e.g., ioExecutor, computeExecutor, mqConsumerExecutor, scheduleExecutor.
Prefer gateway‑level rate limiting over simply increasing Tomcat threads – implement token‑bucket filters or use Sentinel/Resilience4j.
package com.example.demo.web;
import com.google.common.util.concurrent.RateLimiter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class ApiRateLimitFilter implements Filter {
private final RateLimiter orderCreateLimiter = RateLimiter.create(200.0);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if ("/api/orders".equals(httpRequest.getRequestURI()) && "POST".equalsIgnoreCase(httpRequest.getMethod()) && !orderCreateLimiter.tryAcquire()) {
httpResponse.setStatus(429);
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.getWriter().write("{\"code\":429,\"message\":\"Too many requests\"}");
return;
}
chain.doFilter(request, response);
}
}Design DB connection pool to match Tomcat threads – typical HikariCP settings:
spring:
datasource:
hikari:
maximum-pool-size: 40
minimum-idle: 10
connection-timeout: 2000
validation-timeout: 1000
idle-timeout: 300000
max-lifetime: 1200000
leak-detection-threshold: 10000Enforce timeout, pool, and circuit‑breaker on every downstream call – example for OpenFeign with OkHttp:
spring:
cloud:
openfeign:
okhttp:
enabled: true
client:
config:
default:
connectTimeout: 1000
readTimeout: 1500
loggerLevel: basicCache as a throughput amplifier but protect against penetration, breakdown, and avalanche – use a two‑level approach (Caffeine local + Redis distributed) with logical expiration and single‑flight rebuild.
Make logging asynchronous and filter low‑value logs – Logback AsyncAppender example:
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>8192</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>true</neverBlock>
<appender-ref ref="FILE"/>
</appender>Standardise exception, timeout, and degradation responses
package com.example.demo.web;
import java.time.Instant;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleValidation(MethodArgumentNotValidException ex) {
return Map.of(
"code", 400,
"message", ex.getBindingResult().getAllErrors().get(0).getDefaultMessage(),
"timestamp", Instant.now().toString()
);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleException(Exception ex) {
return Map.of(
"code", 500,
"message", "System busy, please try again later",
"timestamp", Instant.now().toString()
);
}
}Validate request parameters before business logic – use Bean Validation annotations on DTOs and enforce them at the controller level.
Implement idempotency for payment/order APIs
package com.example.demo.idempotent;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Aspect
@Component
@RequiredArgsConstructor
public class IdempotentAspect {
private final StringRedisTemplate redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
String key = IdempotentKeyResolver.resolve(joinPoint, idempotent);
Boolean locked = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(idempotent.expireSeconds()));
if (!Boolean.TRUE.equals(locked)) {
throw new IllegalStateException("Duplicate request, please retry later");
}
try {
return joinPoint.proceed();
} finally {
redisTemplate.delete(key);
}
}
}Detect slow requests via AOP
package com.example.demo.observability;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class SlowRequestAspect {
@Around("@within(org.springframework.web.bind.annotation.RestController)")
public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
if (costMs > 500) {
log.warn("slow_request signature={} costMs={}", joinPoint.getSignature(), costMs);
}
}
}
}Expose Actuator + Micrometer metrics for observability
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
probes:
enabled: true
metrics:
tags:
application: order-serviceGraceful shutdown must be coupled with Kubernetes readiness/liveness probes and a pre‑stop hook
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
preStop:
exec:
command: ["sh", "-c", "sleep 15"]Make asynchronous processing a first‑class capability, not a controller‑level @Async trick – offload post‑order actions (points, notifications, audit) to MQ and provide status via polling, WebSocket, or callbacks.
Lifecycle events for start‑up warm‑up, registration, read‑only flag on shutdown, and resource cleanup
package com.example.demo.lifecycle;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ApplicationLifecycleListener {
@EventListener
public void onClosed(ContextClosedEvent event) {
log.info("application closing, start cleaning resources");
UserContextHolder.clear();
}
}12 Core Tomcat Parameters and Their Trade‑offs
maxThreads– maximum worker threads. Too low limits throughput; too high increases context switches and downstream contention. minSpareThreads – minimum idle threads. Small values cause thread creation spikes under burst traffic; large values waste resources. acceptCount – request queue length when workers are busy. Small values reject connections early; large values hide overload by growing queue latency. maxConnections – total allowed TCP connections (including Keep‑Alive). Must be coordinated with OS file‑descriptor limits. connectionTimeout – time to wait for request data after connection establishment. Shorter values free slow or malicious connections faster. keepAliveTimeout – idle time between requests on a persistent connection. Typical 10‑30 s for gateway‑proxied services. maxKeepAliveRequests – how many requests a single Keep‑Alive connection may serve. Increase for high‑frequency internal calls. maxHttpHeaderSize – maximum request‑header size. Important when JWT or many cookies are used. compression – enable response compression (CPU cost vs bandwidth saving). accesslog – enable/disable access logging. When enabled, make it asynchronous to avoid I/O bottlenecks. maxSwallowSize – maximum request body size that Tomcat will swallow on error. Critical for file‑upload endpoints. protocol – choose between Http11NioProtocol (default) and Http11Nio2Protocol. Switch only after benchmark evidence.
Production Case Study: Order Service End‑to‑End Tuning
A typical order‑creation flow (300 QPS normal, 2000 QPS peak) includes inventory check, price calculation, DB write, and payment event publishing. The tuned design separates fast path from async post‑processing:
Client → Rate‑limit → Tomcat → Validation + Idempotency → Local cache snapshot → Inventory RPC (with timeout, circuit‑breaker, bulkhead) → Short DB transaction → MQ event → Immediate responseKey design points:
Idempotency at the entry point prevents duplicate writes.
Product snapshot is read from cache to avoid deep calls.
Database transaction only wraps the core write; external calls stay outside.
Post‑order actions (points, notifications) are sent via MQ, keeping the main request short.
Resulting benefits: reduced worker‑thread hold time, lower DB contention, and eliminated duplicate orders.
Graceful Rolling Upgrade Checklist
Mark Pod as not ready (readiness probe false).
Ingress/Service stops sending new traffic.
preStop hook gives enough time for in‑flight requests to finish.
Spring Boot graceful shutdown waits for active requests.
MQ consumers stop pulling new messages.
Wait for all tasks to complete before process exit.
If 502/504 errors still appear during rollout, check pre‑stop duration, readiness health logic, long‑running requests, and whether MQ consumers are still pulling.
Container‑Native and Cloud‑Native Tuning Highlights
CPU limits matter: a pod limited to 1‑2 CPU cores should not be given 500 Tomcat threads; thread count must be proportional to CPU quota.
JVM memory flags for containers: -XX:+UseContainerSupport, -XX:MaxRAMPercentage=75.0, -XX:InitialRAMPercentage=50.0 to avoid OOMKilled.
HPA should watch more than CPU: monitor tomcat_busy_threads / tomcat_max_threads, P95/P99 latency, request queue time, 5xx rate, and Hikari active‑connection ratio.
Three‑Stage Load‑Testing Methodology
Baseline single‑endpoint test – find raw throughput limits and resource boundaries.
Mixed‑traffic test – simulate realistic API mix, hotspot spikes, cache hit/miss, and downstream failures.
Failure & rollout test – inject DB slow queries, RPC timeouts, Redis jitter, and perform rolling upgrades to verify stability.
Skipping stage three means you only know the system’s performance in a “healthy” state, not its production resilience.
Real‑World Incident Recaps
Case 1 – Threads not full but time‑outs occur: downstream inventory service had a 10 s read timeout, causing workers to block and the gateway to retry, amplifying traffic.
Case 2 – Over‑scaled Tomcat crashes DB: raising maxThreads to 800 without matching DB pool size turned the front‑end into a downstream load generator.
Case 3 – 502/504 during rollout: readiness probe returned true too early and preStop was too short, so old pods still received traffic while shutting down.
Quick Reference Lists (English)
Spring Boot Engineering Techniques (17)
Externalise critical parameters
Tomcat customisation via factory
Offload heavy work to async executor
Isolate thread pools per task type
Gateway‑level rate limiting
DB pool sizing aligned with Tomcat
Downstream timeout & circuit‑breaker
Systematic cache strategy
Asynchronous logging
Unified exception handling
Pre‑validation of request parameters
Idempotency control
Slow‑request monitoring
Actuator + Micrometer observability
Graceful shutdown procedures
MQ‑based async spike reduction
Lifecycle event handling (startup, drain, cleanup)
Tomcat Core Parameters (12)
maxThreads– max worker threads minSpareThreads – min idle threads acceptCount – request queue length maxConnections – total TCP connections connectionTimeout – time to wait for request data keepAliveTimeout – idle time between requests on a persistent connection maxKeepAliveRequests – max requests per Keep‑Alive connection maxHttpHeaderSize – max request‑header size compression – enable response compression accesslog – enable/disable access logging maxSwallowSize – max request body size Tomcat will swallow on error protocol – choose NIO or NIO2 implementation
Actionable Recommendations for Engineers and Architects
Phase 1 – Observability First: expose Prometheus metrics, add time‑outs to all downstream calls, enable rate limiting and idempotency on critical APIs.
Phase 2 – Resource Coordination: jointly tune Tomcat, DB, Redis, and HTTP client pools; move long‑running tasks out of the request path; add caching and degradation for hot endpoints.
Phase 3 – Architectural Evolution: introduce MQ for async processing, perfect graceful rollout workflow, and hook HPA to the richer metric set.
Following this incremental path ensures each change is measurable, reversible, and delivers clear value.
Conclusion
Spring Boot + Tomcat is not inherently fragile under high load; the fragility comes from default‑only configurations, lack of isolation, and missing observability. By treating the request as a full chain, applying the 17 engineering techniques, and carefully adjusting the 12 Tomcat knobs, you can build a production‑grade service that withstands traffic spikes without resorting to emergency “bump the thread count” fixes.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
