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
@Scheduled tasks swallow exceptions — they log and move on, with no notification.
- If your application OOM-kills or restarts, the scheduler stops. No alert fires because there is nothing watching from outside.
- Spring Actuator shows scheduler status but does not tell you if a task produced correct results.
- Crontiq tracks metrics over time and catches regressions: a cleanup that usually deletes 500 rows but suddenly deletes zero.
Add spring.task.scheduling.pool.size=4 to your application.yml if you have multiple scheduled tasks, so they do not block each other.