Spring Boot runs an enormous share of the JVM backends in production — and a Spring Boot deployment is more than one fat JAR. It is Tomcat with a finite request-thread pool, a HikariCP connection pool in front of your database, usually Redis, a JVM with its own memory ceiling, and background work spread across @Scheduled methods, Quartz triggers, or Spring Batch jobs. Any of those can fail while GET / keeps returning 200 — and the JVM adds failure modes all its own, where the app answers slower and slower on its way down long before it stops answering at all.
This guide walks through monitoring a Spring Boot app the way it actually breaks. We will put Actuator's health endpoint to work (Spring Boot is the rare framework that ships a genuinely good one), deal with the management-port and Spring Security gotchas that hide it from monitors, enumerate the failure modes worth alerting on, cover background jobs, and finish with a concrete CronAlert setup you can copy. CronAlert is agentless and runs on Cloudflare's edge, so everything here is just HTTP checks, content assertions, and heartbeats — nothing to add to your classpath.
Actuator gives you a real health endpoint — use it
Unlike FastAPI or Express, where you write your own health route, Spring Boot ships one. Add spring-boot-starter-actuator and you get /actuator/health: an aggregate of auto-configured HealthIndicator beans covering your DataSource, Redis, disk space, mail server, and most other Spring-managed dependencies. When everything passes it returns UP with a 200; when any indicator fails it returns DOWN with a 503 — exactly the contract an external monitor wants, with none of the hand-rolled try/catch plumbing other frameworks require. For the general theory of what belongs in a health check, see our guide to HTTP health check endpoints.
Three configuration decisions matter. First, details exposure: keep management.endpoint.health.show-details=never (or when-authorized) for anonymous callers, so the public response is a bare UP/DOWN without database names or disk paths. Second, probe groups: set management.endpoint.health.probes.enabled=true and you get /actuator/health/liveness and /actuator/health/readiness — liveness says "the process is alive, don't restart me," readiness says "I can serve traffic." Point Kubernetes at those (our Kubernetes monitoring guide covers why confusing them causes restart loops) and point your external monitor at the full /actuator/health or readiness group, which includes dependency checks. Third, custom indicators: implement HealthIndicator for anything the app is useless without — a payment gateway, a downstream API — and it joins the aggregate automatically. The database health endpoint deep-dive covers what a good database check verifies.
Two gotchas hide this endpoint from monitors. If your team sets management.server.port to a separate internal port — a common hardening move — Actuator is no longer reachable from the internet, and neither is your health check. Either keep the health endpoint on the main port and move the rest, or add a thin public /healthz controller that delegates to HealthEndpoint and monitor that. And if Spring Security is in play, a locked-down config returns 401/403 for /actuator/health, which from a monitor's perspective is indistinguishable from downtime: permit exactly the health endpoint — EndpointRequest.to(HealthEndpoint.class) with permitAll() — or have your monitor send a static token header.
What to monitor in a Spring Boot app
One health endpoint is the foundation, not the whole story. Spread checks across the surfaces your users and integrators actually touch:
- The homepage or API root. Whatever real clients hit first, checked for a 200 and for expected content — not just that something responded.
- Your
/actuator/health(or delegating/healthz). The dependency-aware signal that catches database, Redis, and disk trouble. - Your most important endpoints. The two or three routes your product actually depends on, each with a keyword assertion on the JSON — a field name that only appears in a correct response. See monitoring API endpoints for assertion patterns.
- The OpenAPI docs, if you publish them. If integrators rely on
/swagger-uior/v3/api-docs, a broken schema deserves its own check. - HTTPS and the SSL certificate. An expired certificate takes down every client no matter how healthy Tomcat is; see SSL certificate monitoring.
The HTTP and SSL checks here fit comfortably inside CronAlert's free plan — 25 monitors at a 3-minute interval. Pro ($5/mo) tightens the interval to 1 minute and unlocks the keyword and content assertions used below.
Spring Boot-specific failure modes
Generic uptime advice misses the ways JVM apps actually break. Design your checks around these:
- HikariCP pool exhaustion. A connection leak or a batch of slow queries empties the pool; every request that needs the database blocks for
connectionTimeout— 30 seconds by default — then throws. The app turns into a wall of timeouts while process checks stay green. TheDataSourceHealthIndicator catches the unreachable-database case; a response-time threshold catches the strained-pool case earlier.leakDetectionThresholdfinds the leaking code path. - Tomcat thread-pool saturation. Tomcat's default 200 request threads plus a blocking I/O model mean one slow downstream dependency can pin every thread — and then all endpoints slow down together, including ones that never touch that dependency. The signature from outside: global latency creep, then timeouts everywhere at once.
- OutOfMemoryError and GC death spirals. A JVM under memory pressure spends more and more time in garbage collection before it finally dies — responses stretch from 200ms to 2s to 20s over minutes or hours. A response-time threshold is the early-warning system; by the time you get connection refused, the warning window is gone.
- Starved
@Scheduledtasks. All@Scheduledmethods share a single scheduler thread by default. One hung task — a stuck HTTP call with no timeout — silently blocks every other scheduled job in the app. Nothing errors; work just stops happening. - Silently dead schedulers. A thrown exception inside a
@Scheduledmethod is caught and logged, but anErrorcan take the scheduler thread down with it — and either way, jobs can quietly stop after certain failures with nothing in the request path to notice. - Failed migrations at startup. A Flyway or Liquibase migration failure leaves the app half-up: the process exists, the port may answer, but the context never finished starting. A readiness-aware health check refuses to report healthy; a bare port check happily passes.
- Readiness/liveness confusion. Wiring a dependency-heavy readiness check into the liveness probe makes Kubernetes restart pods whenever the database blips — turning a degradation into an outage.
- Actuator 401/403s after a security change. A tightened security config that catches
/actuator/healthmakes monitors report downtime for a perfectly healthy app.
Most of these never change your homepage's status code. That is the recurring lesson across every framework we have covered: status codes lie, and you need dependency-aware checks, content assertions, and response-time thresholds to see the truth.
Monitoring background work: @Scheduled, Quartz, and Spring Batch
Background processing is where JVM monitoring most often falls short. Your endpoints can be flawless while every @Scheduled job is starved behind one hung task, Quartz misfires pile up, or last night's Spring Batch run failed on step three — and no HTTP check will notice, because nobody makes a request that fails. Emails stop, exports stop, syncs stop, and you find out from a customer.
The fix is heartbeat monitoring: your job reaches out instead of the monitor reaching in. Every time a scheduled task completes successfully, it makes a quick HTTP request to a unique CronAlert heartbeat URL. You configure the expected interval plus a grace period, and if the ping stops arriving, CronAlert alerts you that the pipeline went silent — silence is the alarm. This catches the single-thread starvation problem, the dead scheduler, the misfire backlog, and the failed batch step with one mechanism, because they all produce the same observable symptom: the ping stops.
A practical setup: one heartbeat per critical job (nightly billing, hourly sync), pinged as the last line of the task — and for Spring Batch, ping from an afterJob listener only when the exit status is COMPLETED, so a failed run counts as silence. Also give the scheduler a real pool via spring.task.scheduling.pool.size so one slow task cannot starve the rest. Our guides to cron job heartbeat monitoring and background worker monitoring cover the patterns in depth, and for one-shot jobs see batch job monitoring. Heartbeat monitors are available on every paid CronAlert plan, from $5/mo.
Catching "up but wrong" with content checks
A Spring Boot response can be a perfect 200 and still be wrong: a controller returning an empty list because a swallowed exception hid the real error, a stale cache serving yesterday's data, or — a classic — the whitelabel error page rendering with a 200 under certain error-handling misconfigurations, so your "error page" passes every status-code check.
To catch these, assert on the response body. For JSON APIs the pattern is simple: require a string that only a correct response contains — a known field name, an expected enum value, a version marker. If an error payload or an empty template takes its place, the keyword disappears and the check fails despite the 200. You can also assert that an error marker is absent, so a leaked stack trace or the words "Whitelabel Error Page" trip the alarm. Regex matching (Pro) handles dynamic payloads. For endpoints that should be byte-stable, a SHA-256 content-hash check alerts the instant the output drifts. See the keyword monitoring guide for building robust assertions.
Response-time thresholds are your JVM early-warning system
More than most platforms, JVM apps degrade before they die. GC pressure stretches every response; a straining Hikari pool adds seconds of wait; a filling Tomcat thread pool queues requests before it rejects them. Each of those failure curves has a long, monitorable ramp — if you are watching latency. Set a response-time threshold on your health endpoint and your key routes so that a creep past, say, two seconds alerts you while the app is still technically up. The same threshold catches degrading databases, cold caches, and saturated downstream services before they become 504s. Our timeout thresholds guide covers picking numbers that alert early without generating noise.
A concrete CronAlert setup
Here is an end-to-end setup you can replicate in a few minutes after you create a free CronAlert account:
- Expose Actuator health safely. Add
spring-boot-starter-actuator, keepshow-detailsoff for anonymous callers, and make sure the health endpoint is reachable on the public port — viapermitAll()on exactly that endpoint, or a thin/healthzthat delegates. - Create an HTTP monitor for it. Point CronAlert at
https://api.yourapp.com/actuator/health, expect 200, set the interval (3 minutes free, 1 minute on Pro). Actuator's UP/503 contract does the rest. - Monitor your key endpoints with JSON keyword assertions (Pro). Require a field name only present in a correct response; optionally assert "Whitelabel Error Page" is absent from HTML routes.
- Add a heartbeat per critical job (Pro).
@Scheduledtasks ping their CronAlert heartbeat URL as the last line of a successful run; Spring Batch pings from anafterJoblistener onCOMPLETED. - Turn on SSL and response-time checks. Certificate monitoring on the domain; a response-time threshold on the health endpoint and key routes — your GC and pool-exhaustion early warning.
- Wire up alert channels. Email, Slack, Discord, Teams, Telegram, PagerDuty, Opsgenie, Splunk On-Call, webhooks, or PWA push.
Every plan includes the REST API (read-only on free, full read-write from Pro), so you can manage your monitor fleet from a Gradle task or CI pipeline — and CronAlert ships an MCP server so Claude Code, Cursor, or Windsurf can manage monitors for you. Teams needing multi-region coverage can move to the Team plan, which checks from five edge regions with quorum so a single regional blip never pages you.
Frequently asked questions
Does Spring Boot have a built-in health check endpoint?
Yes — add spring-boot-starter-actuator and /actuator/health aggregates auto-configured HealthIndicators (DataSource, Redis, disk space, and more), returning UP/200 or DOWN/503. Enable management.endpoint.health.probes.enabled=true for Kubernetes-style liveness and readiness groups, and implement HealthIndicator for custom checks.
Should /actuator/health be public?
The status can be; the details should not. Keep show-details at never or when-authorized so anonymous callers see only UP/DOWN. And watch the management-port gotcha: management.server.port on an internal port hides the endpoint from your monitor — expose just health on the main port, or monitor a thin delegating /healthz.
Why does my monitor get 401 or 403 from /actuator/health?
Spring Security is protecting Actuator. Permit exactly the health endpoint — EndpointRequest.to(HealthEndpoint.class) with permitAll() — or send a static token header from your monitor. Sudden 401/403s after a security change are a monitoring regression, not an outage.
Why did my @Scheduled tasks stop running?
All @Scheduled tasks share one scheduler thread by default, so a single hung task silently starves every other job. Exceptions are logged and survivable, but an Error can kill the thread. Configure a pool via spring.task.scheduling.pool.size, put timeouts on everything tasks call, and add heartbeats so silence itself alerts you.
What causes HikariCP connection pool exhaustion and how do I catch it?
Connection leaks, slow queries holding connections, or traffic beyond maximum-pool-size. Requests then block for the 30-second default connectionTimeout and throw. Catch it with the DataSource HealthIndicator via /actuator/health plus a response-time threshold that fires during the stall phase; use leakDetectionThreshold to find the leak.
Start monitoring your Spring Boot app
Spring Boot apps fail in ways a single ping never catches: a straining Hikari pool, a saturated Tomcat thread pool, a GC death spiral, @Scheduled jobs starved behind one hung task, and 200s wrapping wrong responses. Actuator's health endpoint plus keyword assertions on key routes, heartbeats on background jobs, and SSL and response-time checks give you a signal you can trust — the HTTP and SSL checks start on CronAlert's free plan, and keyword assertions plus heartbeats come with Pro at $5/mo. Create a free CronAlert account and point your first check at /actuator/health in the next five minutes.
Related reading: HTTP health check endpoints, building a database health endpoint, API endpoint monitoring, and our companion guides for Django, Rails, Laravel, FastAPI, and Express/Node.js applications.