Production Troubleshooting

Introduction

Production issues—startup failures, OutOfMemoryError, slow SQL, thread pool exhaustion—need systematic diagnosis. This chapter maps symptoms to checks: logs, Actuator metrics, thread dumps, and rollback habits so you recover quickly without guessing.

Prerequisites

Startup Failures

Read the root cause at bottom of stack trace.

SymptomCommon causeAction
Port already in useOld process on 8080Kill process or change server.port
Failed to configure DataSourceWrong URL/credentialsFix env vars; test DB from host
Bean creation exceptionMissing dependency, typo in @AutowiredRun with --debug, read condition report
Flyway/Liquibase validationSchema driftAlign migration or repair
bash
java -jar app.jar --debug 2>&1 | tee startup.log

See Startup Flow.

Application Not Healthy

bash
curl -s http://localhost:8080/actuator/health | jq

DOWN components point to failing dependency—Redis, disk space, custom indicator.

Check recent deploy config change before deep debugging.

OutOfMemoryError

Types:

ErrorMeaning
Java heap spaceToo many objects in heap
GC overhead limit exceededGC thrashing
MetaspaceClass loader leak / too many classes

Capture heap dump (once):

bash
jcmd <pid> GC.heap_dump /tmp/heap.hprof

Analyze with Eclipse MAT or similar—find retained largest objects.

Mitigations:

  • Increase -Xmx only after understanding leak
  • Fix unbounded caches
  • Paginate large queries

Slow HTTP / Timeouts

  1. Actuator metricshttp.server.requests p95 latency
  2. Logs — correlate slow request id / trace id
  3. Database — enable slow query log on MySQL
  4. External APIs — timeouts on RestClient (chapter 21)

Thread dump for stuck requests:

bash
jcmd <pid> Thread.print > threads.txt

Look for blocked threads on JDBC pool or locks.

Connection Pool Exhausted

HikariCP messages: Connection is not available, request timed out.

Check:

yaml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      connection-timeout: 30000

Causes:

  • Long @Transactional holding connections
  • Pool size too small for concurrency
  • Connection leak (not closed)— rare with Spring JDBC if used correctly

Thread Pool Exhausted (@Async / Tomcat)

Tomcat max threads default 200—under load all busy.

@Async queue full—rejected executions.

Tune:

yaml
server:
  tomcat:
    threads:
      max: 300
spring:
  task:
    execution:
      pool:
        max-size: 32
        queue-capacity: 500

Fix root cause: slow downstream, not only raise limits.

SQL and ORM Issues

SymptomCheck
N+1 queriesJPA @EntityGraph, fetch join, or DTO query
Full table scanEXPLAIN, add index
Lock wait timeoutShort transactions, index FK columns

Enable SQL log only temporarily in prod.

Emergency Rollback

  1. Stop bad deployment traffic (load balancer drain)
  2. Redeploy previous known-good image/jar
  3. Restore DB from backup if bad migration applied—last resort
  4. Post-incident: timeline, root cause, action items

Keep last three releases artifacts in registry.

Observability During Incident

text
Logs (what happened) → Metrics (how bad) → Traces (which span slow)

Do not enable TRACE globally in prod long-term—disk and CPU cost.

FAQ

Cannot reproduce locally?

Compare profile, data volume, and env vars—prod-only secrets wrong common.

Restart fixes it?

Suspect memory leak or external dependency glitch—still investigate.

Kill -9 safe?

Last resort—prefer graceful shutdown for in-flight requests.

Who owns DB vs app?

Split escalation—network blip vs query regression.

Spring Boot Admin?

Third-party UI aggregating Actuator across instances—optional ops tool.

More learning?

Spring Boot Up and Running and official troubleshooting guides.