Skip to content
LastPing
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.

Your assistant can do this for you

Monitor my Java jobs with LastPing

On this page
  1. The snippets
  2. What this catches
  3. Questions people ask

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.

  • How do I monitor a Spring @Scheduled job?

    Wrap the method body in try/catch: on success, call restTemplate.getForObject on the ping URL; on exception, POST the message to the /fail endpoint and re-throw so Spring can apply its own error handling.

  • How do I monitor a Spring Batch job?

    Implement JobExecutionListener and in afterJob() check jobExecution.getStatus(). If BatchStatus.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. RestTemplate or WebClient from Spring are convenient wrappers 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.

  • 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.

FREE FOR INDIVIDUALS · FULLY HOSTED

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.