Scheduler User Guide
Kaspr Scheduler is an optional add-on component of KasprApp that provides delayed and cron-based message delivery to Kafka topics.
What the Scheduler Does
Scheduler lets producers send a message now and have it delivered to a target topic at a specified time.
Typical use cases:
- Delayed notifications.
- Deferred retries.
- Time-based workflow triggers.
Enable Scheduler
Enable scheduler on KasprApp:
schedulerEnabled: true
apiVersion: kaspr.io/v1alpha1
kind: KasprApp
metadata:
name: orders-app
spec:
config:
schedulerEnabled: trueOptional tuning:
schedulerTopicPartitions: number of partitions for scheduler internal topics (default: 1).schedulerDebugStatsEnabled: enable periodic scheduler stats logs.
Automatically Created Kafka Topics
When scheduler is enabled, the following topics are automatically created:
<app-id>-schedule-requests— Client publishes scheduling actions here.<app-id>-schedule-actions— Internal topic for action processing.<app-id>-schedule-rejections— Invalid requests are sent here.
Topics are created with the number of partitions specified by schedulerTopicPartitions.
Automatically Created Internal Tables
Scheduler uses Kafka-backed changelog tables (stored as internal topics):
<app-id>-timetable— Stores scheduled messages keyed by delivery time.<app-id>-schedule-index— Reverse index mapping request IDs to timetable locations.
Publish a Scheduled Message (ADD)
Send your message to:
<app-id>-schedule-requests
Required headers for ADD:
x-scheduler-deliver-to: destination topic name.x-scheduler-deliver-at: UTC timestamp in ISO format.
Optional headers for ADD:
x-scheduler-action: defaults toADDif omitted.x-scheduler-request-id: client-defined id used later for cancellation.
Example payload shape:
{
"key": "order-123",
"value": {"type": "send-reminder"},
"headers": {
"x-scheduler-deliver-to": "reminders-topic",
"x-scheduler-deliver-at": "2026-05-24T12:30:00Z",
"x-scheduler-request-id": "reminder-order-123"
}
}What to Expect
- If timestamp is in the future, message is delivered at scheduled time.
- If timestamp is in the past, message is delivered immediately.
Replace a Scheduled Message (REPLACE)
Use REPLACE when you want to update an existing scheduled message identified by request id.
To replace, publish to <app-id>-schedule-requests with:
x-scheduler-action: REPLACEx-scheduler-request-id: <same id used at schedule time>x-scheduler-deliver-to: <destination topic>x-scheduler-deliver-at: <new UTC ISO timestamp>
REPLACE requires both target fields (deliver-to, deliver-at) and identity (request-id).
Replace Semantics
- If the request id exists, scheduler removes the previous row and inserts the new row.
- If the request id does not exist, scheduler treats it as a new schedule with that id.
- If the existing schedule is strictly identical (same time, destination, key, value, headers), scheduler performs a no-op replacement.
Example headers:
x-scheduler-action=REPLACE
x-scheduler-request-id=reminder-order-123
x-scheduler-deliver-to=reminders-topic
x-scheduler-deliver-at=2026-05-24T13:00:00ZCancel a Scheduled Message
To cancel, publish to <app-id>-schedule-requests with:
x-scheduler-action: CANCELx-scheduler-request-id: <same id used at schedule time>
No x-scheduler-deliver-at or x-scheduler-deliver-to is required for CANCEL.
Example headers:
x-scheduler-action=CANCEL
x-scheduler-request-id=reminder-order-123Cancel Semantics
- Cancellation is id-based and best effort.
- If the request id is unknown, no message is canceled.
- If the message is already being delivered, cancellation may be too late.
Recurring Scheduled Messages
Scheduler also supports recurring message delivery using cron expressions. A cron fires according to a schedule and automatically re-schedules itself.
Enable Cron
Cron is an optional feature that requires scheduler to be enabled. Enable it on KasprApp:
schedulerCronEnabled: true
apiVersion: kaspr.io/v1alpha1
kind: KasprApp
metadata:
name: orders-app
spec:
config:
schedulerEnabled: true
schedulerCronEnabled: trueAutomatically Created Internal Tables
When cron is enabled, scheduler creates additional Kafka-backed changelog tables:
<app-id>-cron-registry— Stores cron definitions (expression, destination, policy, status).<app-id>-cron-due-index— Efficient index for finding crons due.
Create a Cron
Send a message to <app-id>-schedule-requests with:
x-scheduler-action: CRON_ADDx-scheduler-cron-expr: valid cron expression (e.g.,0 */5 * * * *for every 5 minutes).x-scheduler-deliver-to: destination topic name.x-scheduler-request-id: unique cron id (used for pause/resume/cancel).x-scheduler-cron-missed-fire-policy(optional):"replay"(default) or"skip".
Example headers:
x-scheduler-action=CRON_ADD
x-scheduler-request-id=hourly-report-cron
x-scheduler-cron-expr=0 * * * * *
x-scheduler-deliver-to=reports-topic
x-scheduler-cron-missed-fire-policy=replayMissed Fire Policy
When scheduler recovers from downtime or a cron is resumed, the missed_fire_policy determines how to handle fires that occurred while the cron was offline:
- “replay” (default): Materialize all missed fires. Use for work that must happen even if delayed (e.g., daily reports, reconciliation jobs).
- “skip”: Advance the cron to now and skip missed fires. Use for periodic status checks or lightweight monitoring tasks where historical backfill is unnecessary.
Pause a Cron
Pause a cron to temporarily stop firing:
x-scheduler-action: CRON_PAUSEx-scheduler-request-id: <cron id>
Example headers:
x-scheduler-action=CRON_PAUSE
x-scheduler-request-id=hourly-report-cronResume a Cron
Resume a paused cron. The cron respects its missed_fire_policy:
- “replay”: Materialize any fires that occurred while paused.
- “skip”: Advance to now, skipping the pause gap.
Send:
x-scheduler-action: CRON_RESUMEx-scheduler-request-id: <cron id>
Example headers:
x-scheduler-action=CRON_RESUME
x-scheduler-request-id=hourly-report-cronCancel a Cron
Permanently remove a cron:
x-scheduler-action: CRON_CANCELx-scheduler-request-id: <cron id>
Example headers:
x-scheduler-action=CRON_CANCEL
x-scheduler-request-id=hourly-report-cronCron Semantics and Guarantees
- Self-healing: Crons automatically recover after arbitrary-length downtime (including service restarts). The missed_fire_policy determines whether missed fires are replayed or skipped.
- No global ordering: Fires are delivered at-least-once per partition; partition ordering is preserved but not global.
- Minimum interval: Cron expressions must fire at least 5 seconds apart (configurable via
schedulerCronMinIntervalSeconds).
Cron Examples
Daily Report at 9 AM UTC
x-scheduler-request-id=daily-report-cron
x-scheduler-cron-expr=0 9 * * * *
x-scheduler-deliver-to=reports-topic
x-scheduler-cron-missed-fire-policy=replayEvery 5 Minutes
x-scheduler-request-id=monitor-check-cron
x-scheduler-cron-expr=*/5 * * * * *
x-scheduler-deliver-to=monitoring-topic
x-scheduler-cron-missed-fire-policy=skipEvery Hour on the Hour
x-scheduler-request-id=hourly-sync-cron
x-scheduler-cron-expr=0 * * * * *
x-scheduler-deliver-to=sync-topic
x-scheduler-cron-missed-fire-policy=replayInvalid Requests and Rejections
Invalid scheduler requests are sent to:
<app-id>-schedule-rejections
Common rejection reasons for regular schedules (ADD/REPLACE):
- Missing
x-scheduler-deliver-atforADD. - Missing
x-scheduler-deliver-toforADD. - Missing
x-scheduler-deliver-atforREPLACE. - Missing
x-scheduler-deliver-toforREPLACE. - Invalid timestamp format for
x-scheduler-deliver-at. - Missing
x-scheduler-request-idforCANCEL. - Missing
x-scheduler-request-idforREPLACE.
Common rejection reasons for crons:
- Missing or invalid
x-scheduler-cron-exprforCRON_ADD. - Cron expression fires faster than minimum interval (default 5 seconds).
- Missing
x-scheduler-deliver-toforCRON_ADD. - Missing
x-scheduler-request-idforCRON_ADD,CRON_PAUSE,CRON_RESUME, orCRON_CANCEL. - Invalid
x-scheduler-cron-missed-fire-policy(must be"replay"or"skip").
Rejection records include original key/value/headers plus error details.
Delivery Guarantees and Expectations
- Treat delivery as at-least-once: consumers should be idempotent.
- Do not rely on global ordering across partitions.
- Ensure destination topics exist before scheduling traffic.
Production Checklist
- Use UTC timestamps end to end.
- Include
x-scheduler-request-idon all scheduled messages if you need cancellation or replacement. - Start with default partitioning, then increase
schedulerTopicPartitionsfor higher throughput. - Monitor rejections topic and scheduler lag/throughput metrics.
- Keep consumer handlers idempotent to handle duplicate deliveries safely.
- For crons: choose
missed_fire_policybased on your semantics (replay for “must happen” work, skip for monitoring/checks). - For crons: set
x-scheduler-request-idto a stable, predictable id so you can pause/resume/cancel them.
Quick Troubleshooting
Message was not delivered
- Check destination topic exists.
- Check consumer group is healthy.
- Check if request was rejected in
<app-id>-schedule-rejections.
Message was rejected
- Validate required headers for selected action (
ADD,CANCEL, orREPLACE). - Validate timestamp is parseable ISO UTC.
Cancel did not stop delivery
- Confirm
x-scheduler-request-idexactly matches original scheduled request. - Confirm cancel arrived before dispatch window.
Replace had no visible effect
- Confirm
x-scheduler-request-idmatches the existing scheduled message. - Check
kms_messages_replace_noopto see if replacement was a strict no-op. - Validate replacement timestamp/topic differ from the existing entry when change is expected.
Cron was not created
- Check if scheduler and cron are both enabled.
- Validate
x-scheduler-cron-expris a valid cron expression (e.g.,0 * * * * *). - Check if cron expression fires too frequently (minimum 5 seconds).
- Check if request was rejected in
<app-id>-schedule-rejections.
Cron fires are missing after downtime
- This is normal behavior with
missed_fire_policy=skip. Cron advances to now instead of replaying. - If you need missed fires, change cron to use
missed_fire_policy=replayby creating a new cron with the same id. - Check if cron was paused during downtime (check logs for CRON_PAUSE).
Cron paused, then resumed but fires are not caught up
- Confirm the cron has
missed_fire_policy=replayto backfill the pause gap. - If cron uses
missed_fire_policy=skip, it advances to now without backfill on resume.
Quick Reference
Action: ADD
- Topic:
<app-id>-schedule-requests - Required headers:
x-scheduler-deliver-to,x-scheduler-deliver-at - Optional headers:
x-scheduler-action=ADD,x-scheduler-request-id
Action: CANCEL
- Topic:
<app-id>-schedule-requests - Required headers:
x-scheduler-action=CANCEL,x-scheduler-request-id
Action: REPLACE
- Topic:
<app-id>-schedule-requests - Required headers:
x-scheduler-action=REPLACE,x-scheduler-request-id,x-scheduler-deliver-to,x-scheduler-deliver-at
Action: CRON_ADD
- Topic:
<app-id>-schedule-requests - Required headers:
x-scheduler-action=CRON_ADD,x-scheduler-request-id,x-scheduler-cron-expr,x-scheduler-deliver-to - Optional headers:
x-scheduler-cron-missed-fire-policy(default:replay)
Action: CRON_PAUSE
- Topic:
<app-id>-schedule-requests - Required headers:
x-scheduler-action=CRON_PAUSE,x-scheduler-request-id
Action: CRON_RESUME
- Topic:
<app-id>-schedule-requests - Required headers:
x-scheduler-action=CRON_RESUME,x-scheduler-request-id - Note: Respects cron’s
missed_fire_policyfor handling pause gap.
Action: CRON_CANCEL
- Topic:
<app-id>-schedule-requests - Required headers:
x-scheduler-action=CRON_CANCEL,x-scheduler-request-id