CrontiqIntegrations › Spring Boot

Monitor Spring Boot @Scheduled Tasks with Crontiq

Spring Boot's @Scheduled annotation makes it easy to run periodic tasks inside your application. The downside: these tasks run in-process with no external visibility. If the task throws an exception, Spring catches it and logs a stack trace — but nobody gets paged. If the application restarts and the scheduler does not recover, nobody notices.

Crontiq adds external observability to your scheduled tasks. Each task pings Crontiq on completion. If a ping is late or missing, you get an email alert. If the task reports JSON metrics, Crontiq detects anomalies automatically using a moving average algorithm.

Basic: Ping After Task Completion

Use Spring's RestClient (Spring Boot 3.2+) to send a ping after your task runs. The call is lightweight and non-blocking to your main logic.

import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; @Component public class InvoiceSyncTask { private final RestClient restClient = RestClient.create(); private static final String CRONTIQ_URL = "https://ping.crontiq.io/p/%s/invoice-sync"; @Value("${crontiq.api-key}") private String apiKey; @Scheduled(cron = "0 0 2 * * *") // Every day at 02:00 public void syncInvoices() { int count = invoiceService.syncAll(); // Ping Crontiq with metrics restClient.post() .uri(CRONTIQ_URL.formatted(apiKey)) .contentType(MediaType.APPLICATION_JSON) .body(Map.of("invoices_synced", count)) .retrieve() .toBodilessEntity(); } }

Add your API key to application.yml:

crontiq: api-key: cq_live_80381902d7b36613

With Start Signal and Error Handling

For tasks that take more than a few seconds, signal the start so Crontiq can track duration. Catch exceptions and report them to the /fail endpoint.

@Component public class DataExportTask { private final RestClient crontiq = RestClient.create(); @Value("${crontiq.api-key}") private String apiKey; @Scheduled(cron = "0 30 3 * * MON-FRI") public void exportData() { String baseUrl = "https://ping.crontiq.io/p/" + apiKey + "/data-export"; // Signal start ping(baseUrl + "/start"); try { long start = System.currentTimeMillis(); ExportResult result = exportService.runExport(); long duration = System.currentTimeMillis() - start; // Report success with metrics crontiq.post() .uri(baseUrl) .contentType(MediaType.APPLICATION_JSON) .body(Map.of( "rows_exported", result.rowCount(), "duration_ms", duration, "file_size_kb", result.fileSizeKb() )) .retrieve() .toBodilessEntity(); } catch (Exception e) { // Report failure ping(baseUrl + "/fail"); log.error("Data export failed", e); } } private void ping(String url) { try { crontiq.get().uri(url).retrieve().toBodilessEntity(); } catch (Exception ignored) { // Never let monitoring break the task } } }

Reusable Crontiq Service Bean

If you have multiple scheduled tasks, extract the ping logic into a reusable Spring bean. This keeps your task classes focused on business logic.

@Service public class CrontiqPinger { private final RestClient client = RestClient.create(); @Value("${crontiq.api-key}") private String apiKey; private String url(String slug, String action) { String base = "https://ping.crontiq.io/p/" + apiKey + "/" + slug; return action.isEmpty() ? base : base + "/" + action; } public void start(String slug) { safePing(url(slug, "start")); } public void success(String slug, Map<String, Object> metrics) { try { client.post() .uri(url(slug, "")) .contentType(MediaType.APPLICATION_JSON) .body(metrics) .retrieve() .toBodilessEntity(); } catch (Exception ignored) {} } public void fail(String slug) { safePing(url(slug, "fail")); } private void safePing(String u) { try { client.get().uri(u).retrieve().toBodilessEntity(); } catch (Exception ignored) {} } } // Usage in any scheduled task: @Component @RequiredArgsConstructor public class CleanupTask { private final CrontiqPinger crontiq; @Scheduled(fixedRate = 3600000) public void cleanup() { crontiq.start("cleanup"); try { int deleted = cleanupService.run(); crontiq.success("cleanup", Map.of("deleted", deleted)); } catch (Exception e) { crontiq.fail("cleanup"); } } }

Why External Monitoring Matters for Spring Boot

Add spring.task.scheduling.pool.size=4 to your application.yml if you have multiple scheduled tasks, so they do not block each other.

Start monitoring @Scheduled tasks — free