GUIDE · JAVA · FREE

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.

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 /fail ping opens an incident immediately with the exception message stored on the ping event.
  • A Spring Batch FAILED status — the JobExecutionListener collects 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.

  • Do I need a dependency like OkHttp or Apache HttpClient?

    No. java.net.http.HttpClient (Java 11+) handles GET and POST without additional dependencies. RestTemplate or WebClient from Spring are convenient but not required.

  • What if Spring's scheduler swallows the exception?

    Spring's default TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER logs the exception but suppresses it so the scheduler keeps running. The /fail ping still fires because it is sent inside the catch block before re-throwing. Configure a custom TaskScheduler error handler if you need further propagation.

  • How do I monitor a Quartz job in Java?

    Implement a JobListener and add ping calls in jobWasExecuted — check jobExecutionException for null to distinguish success from failure. The ping URL is the same; only the hook mechanism differs.

  • Is this free?

    Yes. LastPing is free and fully hosted with no monitor cap and no paid tier. See how it compares to Healthchecks.io and Cronitor.

FREE · FULLY HOSTED · NO PAID TIER

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.