DocumentationUser GuideScheduler - Delayed Event Handling

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: true

Optional 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 to ADD if 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: REPLACE
  • x-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:00Z

Cancel a Scheduled Message

To cancel, publish to <app-id>-schedule-requests with:

  • x-scheduler-action: CANCEL
  • x-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-123

Cancel 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: true

Automatically 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_ADD
  • x-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=replay

Missed 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_PAUSE
  • x-scheduler-request-id: <cron id>

Example headers:

x-scheduler-action=CRON_PAUSE
x-scheduler-request-id=hourly-report-cron

Resume 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_RESUME
  • x-scheduler-request-id: <cron id>

Example headers:

x-scheduler-action=CRON_RESUME
x-scheduler-request-id=hourly-report-cron

Cancel a Cron

Permanently remove a cron:

  • x-scheduler-action: CRON_CANCEL
  • x-scheduler-request-id: <cron id>

Example headers:

x-scheduler-action=CRON_CANCEL
x-scheduler-request-id=hourly-report-cron

Cron 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=replay

Every 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=skip

Every 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=replay

Invalid 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-at for ADD.
  • Missing x-scheduler-deliver-to for ADD.
  • Missing x-scheduler-deliver-at for REPLACE.
  • Missing x-scheduler-deliver-to for REPLACE.
  • Invalid timestamp format for x-scheduler-deliver-at.
  • Missing x-scheduler-request-id for CANCEL.
  • Missing x-scheduler-request-id for REPLACE.

Common rejection reasons for crons:

  • Missing or invalid x-scheduler-cron-expr for CRON_ADD.
  • Cron expression fires faster than minimum interval (default 5 seconds).
  • Missing x-scheduler-deliver-to for CRON_ADD.
  • Missing x-scheduler-request-id for CRON_ADD, CRON_PAUSE, CRON_RESUME, or CRON_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-id on all scheduled messages if you need cancellation or replacement.
  • Start with default partitioning, then increase schedulerTopicPartitions for higher throughput.
  • Monitor rejections topic and scheduler lag/throughput metrics.
  • Keep consumer handlers idempotent to handle duplicate deliveries safely.
  • For crons: choose missed_fire_policy based on your semantics (replay for “must happen” work, skip for monitoring/checks).
  • For crons: set x-scheduler-request-id to 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, or REPLACE).
  • Validate timestamp is parseable ISO UTC.

Cancel did not stop delivery

  • Confirm x-scheduler-request-id exactly matches original scheduled request.
  • Confirm cancel arrived before dispatch window.

Replace had no visible effect

  • Confirm x-scheduler-request-id matches the existing scheduled message.
  • Check kms_messages_replace_noop to 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-expr is 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=replay by 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=replay to 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_policy for handling pause gap.

Action: CRON_CANCEL

  • Topic: <app-id>-schedule-requests
  • Required headers: x-scheduler-action=CRON_CANCEL, x-scheduler-request-id