This episode covers scheduled jobs in Quarkus: the Quarkus Scheduler with @Scheduled, building batch processing, task executor and concurrency control configuration, and job monitoring and retry handling.

Many backend tasks happen without a user request: sending emails, cleaning up expired files, pulling data from external APIs, or generating daily reports. These kinds of jobs run on a schedule in the background.
Episode 11 covers the Quarkus Scheduler for scheduled tasks, how to build reliable batch processing, task executor and concurrency control configuration, as well as job monitoring and retry strategies.
./mvnw quarkus:add-extension -Dextensions=schedulerThe command ./mvnw quarkus:add-extension -Dextensions=scheduler adds the scheduler dependency to your project.
Methods marked @Scheduled are run automatically by the scheduler:
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ReportJob {
@Scheduled(every = "60s")
void buatLaporan() {
System.out.println("Membuat laporan terjadwal");
}
}@Scheduled(every = "60s") runs the method every 60 seconds. The duration format uses ISO 8601 — 10s, 1m, 1h, or combinations.
For more precise schedules, use cron:
import io.quarkus.scheduler.Scheduled;
@Scheduled(cron = "0 30 2 * * ?")
void backupMalam() {
// runs every day at 02:30
}The expression "0 30 2 * * ?" means second 0, minute 30, hour 2, every day. Quarkus cron uses the Quartz format — six or seven fields including seconds.
Batch processing should process data in small chunks so it doesn't exhaust memory. The pagination pattern is very useful:
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@ApplicationScoped
public class CleanupJob {
@Inject
SessionRepository sessionRepository;
@Scheduled(cron = "0 0 4 * * ?")
@Transactional
void bersihkanSessionKadaluarsa() {
int halaman = 0;
int batchSize = 500;
List<Session> batch;
do {
PanacheQuery<Session> query =
sessionRepository.find("expiredAt < ?1", now());
batch = query.range(halaman * batchSize,
(halaman + 1) * batchSize - 1).list();
sessionRepository.delete(batch);
halaman++;
} while (!batch.isEmpty());
}
}The query.range(start, end) pattern processes data in chunks of 500 rows. @Transactional on the batch method makes one transaction per execution — if it fails halfway, the whole batch is rolled back.
The Quarkus Scheduler runs on top of an executor:
quarkus.scheduler.threads=5
quarkus.scheduler.overdue-grace-period=30Squarkus.scheduler.threads=5 determines the number of threads available for jobs. The default of 10 is enough for most cases.
A job that runs longer than its interval can pile up. Control it by skipping while still running:
quarkus.scheduler.concurrent-execution.skip-on-concurrent=trueWith skip-on-concurrent=true, if the previous execution hasn't finished, the next one is skipped. An alternative in code: use an AtomicBoolean with compareAndSet(false, true) to lock the execution and release the lock in a finally block.
A job that calls an external service can fail temporarily. Use retry:
import io.quarkus.scheduler.Scheduled;
import org.eclipse.microprofile.faulttolerance.Retry;
import jakarta.enterprise.context.ApplicationScoped;
import java.io.IOException;
@ApplicationScoped
public class SyncJob {
@Scheduled(every = "5m")
@Retry(maxRetries = 3, delay = 2000, retryOn = {IOException.class})
void sinkronisasiExternal() {
panggilApiEksternal();
}
}@Retry(maxRetries = 3, delay = 2000) retries up to three times with a 2-second pause when an IOException occurs.
The Quarkus Scheduler exposes execution metrics at /q/metrics — check them with curl http://localhost:8080/q/metrics | grep scheduler. The metrics cover the number of successful, failed, and overdue executions. For better visibility, log the start and end of every job: clear logs help you trace problem jobs in production.
Episode 11 makes your application work on its own: understanding the Quarkus Scheduler with @Scheduled and cron, building batch processing with pagination and transactions, controlling concurrency with executors and atomic flags, and handling retries and job monitoring.
Key takeaways:
@Scheduled(every = "60s") for intervals, @Scheduled(cron = "...") for precise schedules.quarkus.scheduler.concurrent-execution.skip-on-concurrent prevents overlap.AtomicBoolean gives you control over concurrent execution in code.@Retry from SmallRye Fault Tolerance handles temporary failures./q/metrics.In episode 12 we'll cover basic security and auth — an introduction to Quarkus Security and HTTP authentication, role-based access control and identity stores, authentication mechanism configuration, as well as CSRF, CORS, and secure headers protection.