A step-by-step tutorial builds a reliable Cloudflare scheduled job that checks pricing pages using cron triggers, Cloudflare Queues, and D1. The core problem is that cron triggers and queues only guarantee at-least-once execution, so the job must be idempotent. The approach uses a deterministic 'work ID' derived from the target and its due slot, D1 tables with unique constraints to prevent duplicate work, an expiring lease with a random token to allow safe recovery from crashed Workers, a D1 batch transaction to save results and advance the schedule atomically before acknowledging the queue message, and cadence-preserving logic that calculates the next run from the original due slot rather than the current time. It also covers distinguishing retryable from permanent failures and using exponential backoff with a dead letter queue.
Table of contents
Why a cron trigger is not enoughGive every scheduled check an identityStore the schedule in D1Configure the cron and queueEnqueue the due targetsClaim the due slot with a leaseWhy the lease must expireCheck for completed workSave the result before acknowledgingPreserve the original cadenceRetry only transient failuresWhat each protection doesQuestions this post answers
How do I prevent duplicate processing when Cloudflare Queues delivers the same message twice?
Give each scheduled check a deterministic work ID derived from the target ID and its due slot timestamp, not from when the consumer starts. Store completed checks in a D1 table with the work ID as primary key plus a unique index on target and scheduled time, so duplicate deliveries recognize existing work and skip it safely. daily.dev surfaces patterns like this for engineers designing at-least-once queue consumers.
How can I stop two Cloudflare Queue consumers from processing the same scheduled job at the same time?
Use a conditional UPDATE in D1 that sets a random lease token and an expiration time only when no active lease exists, so only one consumer can claim a given due slot. The lease must expire (for example after 5 minutes) so a crashed Worker doesn't leave the target locked forever, and the token in the WHERE clause prevents a slow old consumer from clearing a newer lease. track locking patterns like this on daily.dev when building reliable background jobs.
How do I keep a recurring job on schedule after a delayed run instead of drifting later each time?
Calculate the next run from the original due slot instead of from the current time, using elapsed time divided by the interval to skip missed slots while preserving cadence. For example, a daily 08:00 job that actually runs on August 6 at 10:00 after being due August 4 should be rescheduled for August 7 at 08:00, not three hours later than the original time. daily.dev helps developers keep track of scheduling patterns like fixed-cadence recovery.