Monitor Java scheduled jobs: Spring @Scheduled and Spring Batch with LastPing
For @Scheduled methods: wrap the body in try/catch, GET
the ping URL on success, POST the exception message to
/fail and re-throw on failure. For Spring Batch: add a
JobExecutionListener that checks BatchStatus
and pings accordingly. Absence detection covers JVM OOM kills and the
pod that never started. No extra dependencies: java.net.http
works from Java 11. Free, no monitor cap.
Your assistant can do this for you
Monitor my Java jobs with LastPing
On this page
The snippets
Create a monitor in the console, set the schedule to match your cron expression plus a grace window, copy the ping URL, and add it to your Spring bean.
Spring @Scheduled with RestTemplate
Inject the ping URL from application.properties.
Re-throwing after the /fail ping lets Spring's
scheduler exception handler and log the failure:
// application.properties:
// lastping.url=https://ping.lastping.dev/<your-monitor-id>
@Component
public class DailyReportJob {
@Value("${lastping.url}")
private String pingUrl;
private final RestTemplate http = new RestTemplate();
@Scheduled(cron = "0 0 2 * * *")
public void run() {
try {
doWork(); // your job
http.getForObject(pingUrl, Void.class);
} catch (Exception e) {
String msg = e.getMessage();
if (msg == null) msg = e.getClass().getName();
http.postForObject(
pingUrl + "/fail",
msg.substring(0, Math.min(msg.length(), 10_000)),
Void.class
);
throw e; // re-throw so Spring logs the failure
}
}
private void doWork() { /* your logic */ }
}
Spring @Scheduled with Java 11 HttpClient (no extra deps)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
@Component
public class NightlyEtlJob {
@Value("${lastping.url}")
private String pingUrl;
private static final HttpClient HTTP = HttpClient.newHttpClient();
private void ping(String suffix, String body) throws Exception {
var req = body == null
? HttpRequest.newBuilder(URI.create(pingUrl + suffix)).GET().build()
: HttpRequest.newBuilder(URI.create(pingUrl + suffix))
.POST(HttpRequest.BodyPublishers.ofString(
body.substring(0, Math.min(body.length(), 10_000)),
StandardCharsets.UTF_8))
.build();
HTTP.send(req, HttpResponse.BodyHandlers.discarding());
}
@Scheduled(cron = "0 30 1 * * *")
public void run() {
try {
runEtl(); // your job
ping("", null);
} catch (Exception e) {
try { ping("/fail", e.getMessage()); } catch (Exception ignored) {}
throw new RuntimeException(e);
}
}
private void runEtl() { /* your logic */ }
}
Spring Batch: JobExecutionListener
For Spring Batch jobs, implement JobExecutionListener
and wire it as a bean. afterJob fires after every run,
and BatchStatus tells you whether to ping success or
failure. Register it in your @Bean Job builder:
import org.springframework.batch.core.*;
import org.springframework.web.client.RestTemplate;
@Component
public class LastPingBatchListener implements JobExecutionListener {
@Value("${lastping.url}")
private String pingUrl;
private final RestTemplate http = new RestTemplate();
@Override
public void afterJob(JobExecution jobExecution) {
if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
http.getForObject(pingUrl, Void.class);
} else {
String msg = jobExecution.getAllFailureExceptions()
.stream()
.map(Throwable::getMessage)
.filter(m -> m != null)
.collect(java.util.stream.Collectors.joining("\n"));
http.postForObject(
pingUrl + "/fail",
msg.substring(0, Math.min(msg.length(), 10_000)),
Void.class
);
}
}
}
// In your @Configuration:
@Bean
public Job etlJob(JobRepository repo, Step step, LastPingBatchListener listener) {
return new JobBuilder("etlJob", repo)
.listener(listener)
.start(step)
.build();
}
Why re-throw matters in @Scheduled
Re-throwing keeps Spring's
TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER from silently
swallowing the exception. With a re-throw the scheduler logs the
stack trace and, if you've configured
@EnableAsync or a custom
AsyncUncaughtExceptionHandler, your alerting pipeline
still fires. The LastPing ping adds fast alerting;
it doesn't replace Spring's error propagation. See also the
cron job guide and the
LastPing MCP server for AI-agent integration.
What this catches
The catch reports what it can; the silence reports the rest.
- A thrown exception: the
/failping opens an incident immediately with the exception message stored on the ping event. - A Spring Batch FAILED status: the
JobExecutionListenercollects all failure exceptions and POSTs them to/fail. - JVM OOM kill: the process sends no ping; the grace window raises the alert without any working code on the failure side.
- The job never ran: pod crash-loop, missing cron entry, dead host. Absence → incident.
- Recovery: the next successful run closes the incident and sends a recovery notice automatically.
Questions people ask
Front-loaded answers: the most important fact first.
-
How do I monitor a Spring @Scheduled job?
Wrap the method body in try/catch: on success, call
restTemplate.getForObjecton the ping URL; on exception, POST the message to the/failendpoint and re-throw so Spring can apply its own error handling. -
How do I monitor a Spring Batch job?
Implement
JobExecutionListenerand inafterJob()checkjobExecution.getStatus(). IfBatchStatus.COMPLETED, GET the ping URL. Otherwise collect the failure exceptions, join them, and POST the text to/fail. -
Do I need a dependency like OkHttp or Apache HttpClient?
No.
java.net.http.HttpClient(Java 11+) handles both GET and POST without additional dependencies.RestTemplateorWebClientfrom Spring are convenient wrappers but not required. -
What if Spring's scheduler swallows the exception?
Spring's default
TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLERlogs the exception but suppresses it so the scheduler keeps running. The/failping still fires because it is sent inside the catch block before re-throwing. Configure a customTaskSchedulererror handler if you need further propagation. -
How do I monitor a Quartz job in Java?
Implement a
JobListenerand add ping calls injobWasExecuted: checkjobExecutionExceptionfor null to distinguish success from failure. The ping URL is the same; only the hook mechanism differs. -
What if the JVM is OOM-killed?
An OOM-killed JVM sends no ping; the missing ping opens an incident after the grace window. This is the core value of absence-detection monitoring for JVM workloads.
-
Is Java job monitoring free?
Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.
The JVM logs the exception. LastPing wakes you up.
One listener now beats debugging a missed nightly batch later. First alert wired up in about a minute, and LastPing monitors itself the same way.