Background jobs and analytics
Trace Queue, Reaction, Cron, and analytics execution to distinguish successful message delivery from completed work.
Background job issues often appear as a successful request with no result. Identify which queue handles the task, then trace its execution records. Different background jobs do not all use the same message processing flow.
1. Find the queue responsible for the task
| Binding | Name variable | Current purpose |
|---|---|---|
ASYNC_POLICY_TASK_QUEUE | ASYNC_POLICY_TASK_QUEUE_NAME | Asynchronous Command execution for Reaction Events |
ASYNC_LOGGER_QUEUE | ASYNC_LOGGER_QUEUE_NAME | Asynchronous log processing and persistence |
ANALYTICS_QUEUE | ANALYTICS_QUEUE_NAME | First-party analytics message processing |
src/entry/index.ts compares the incoming Queue name against these variables for an exact match, then dispatches to the corresponding consumer.
For each queue, check three places: the target name in queues.producers, the name in queues.consumers, and the corresponding *_QUEUE_NAME in vars. All three must match. When the CLI rewrites project resource names, it also updates the related variables.
npm run doctor or npm run doctor:remote can check these configuration relationships. To confirm that the queue exists, messages arrive, and the consumer runs without errors, inspect Cloudflare's queues and Worker logs.
2. Messages arrive, but no result appears
Check each stage in this sequence:
Emit a business event → Save the execution record → Send a Queue message
→ Dispatch through the Worker consumer → Execute the Command → Save the resultSuccessful queue.send() completion only means the message was sent. Acknowledgment does not always mean the task succeeded either. A consumer may acknowledge a message and log an alert when it detects an invalid message, exhausts retries, or requires manual review, preventing endless re-execution.
If the application's asynchronous logging also has problems, inspect Worker runtime logs as well as the dashboard's log pages. Otherwise, a logging queue failure may hide errors from the task queue.
3. Query Events and Commands
Find the execution ID through the dashboard's Reaction pages or logs. You can also query recent matching events in the application database, DB. This example uses one-time payment events:
SELECT id, event_type, event_version, user_id,
created_at, lease_expires_at
FROM event_execution
WHERE event_type = 'payment_succeeded'
ORDER BY created_at DESC
LIMIT 20;Then use the actual execution ID to query Commands:
SELECT id, command_key, command_type, mode, status,
attempt_count, max_attempts, sequence,
created_at, updated_at, completed_at
FROM command_execution
WHERE event_execution_id = 'replace_with_event_execution_id'
ORDER BY sequence;This internal Event execution ID is not Stripe's evt_... ID. Do not mix them up or export the full payload simply for convenience.
event_execution has no persisted status field. The displayed Event status is calculated from Command statuses. Querying event_execution.status produces a missing-column error.
| Command status | Meaning | What to investigate |
|---|---|---|
pending | Not yet finished, possibly waiting for execution or a retry | Queue, lease, execution order, and retry scheduling |
succeeded | The handler returned success, and the result was saved | Continue checking the final outcome you need to verify |
failed | The handler returned a business failure | Use the error code in result to check inputs and business conditions |
errored | The handler threw an exception and finished | Inspect last_error and exception logs in a controlled environment |
If an Event does not allow processing to continue after failure, an earlier failure may block later steps. Later Commands remaining pending do not necessarily indicate lost Queue messages.
4. Distinguish the two retry layers
Commands have their own maxAttempts and execution counts. Current definitions default to a maximum of 3 attempts. Whether another attempt follows a failure also depends on whether the error is retryable.
Cloudflare Queues track delivery attempts separately. The template's policy task consumer has max_retries set to 3, allowing up to three additional deliveries after the first. This is not the Command attempt count. Multiplying the two limits does not give a guaranteed execution count.
When changing the policy Queue's max_retries, also check the maxRetries parameter of consumeEventCommandBatch. The current entry point uses the function's default of 3, which the consumer uses to identify the final delivery attempt. Changing only the Wrangler configuration may make these checks inconsistent.
The lease stays busy
Events use leases to prevent multiple consumers from executing them at the same time. For Event execution lease is busy, compare the current time with lease_expires_at and inspect the related execution logs to determine whether another request is still working.
Do not clear lease fields to force concurrent execution. If the lease remains busy until delivery attempts are exhausted, the consumer logs an alert. Check the actual runtime state instead of assuming that waiting will always lead to automatic recovery.
Command outcome needs manual review
This message means the handler finished, but its result could not be saved reliably. An external email, notification, or other operation may already have occurred. Further delivery could repeat the effect, so the consumer stops automatic retries and raises an alert.
Confirm the outcome with the relevant provider or business records before deciding how to recover the execution record. Do not rerun the task immediately just because the database still shows the old status.
Retry manually after fixing the issue
The template provides a Command retry action in the dashboard's Reaction section. Fix the credentials, inputs, or service failure first, then check whether the previous attempt caused side effects before retrying through the access-protected action.
If it returns 409, inspect the specific message. The current endpoint rejects retries of successful Commands and Events with an active lease. It also requires you to resolve earlier Commands blocking the current step first. Follow these checks rather than bypassing them.
A retry enters the execution flow of the Command's Event. Check whether other steps in that Event will also continue. Do not directly set the database status to pending or treat message redelivery as a safe recovery method for every failure.
5. Cron does not run
The current schedules and dispatch conditions in src/entry/index.ts are:
| Cron expression | Task |
|---|---|
0 0 * * * | Schedule processing for recurring entitlements that are due |
0 1 * * * | Schedule general data cleanup |
30 */6 * * * | Apply the analytics data retention policy |
Cloudflare Cron uses UTC. Convert times when troubleshooting. See Cron Triggers.
The entry point selects a task by its expression string. If you change the expression in wrangler.jsonc without updating the dispatch condition in the source, the platform may invoke the Worker without entering the intended task branch.
Check the platform's trigger records first, then execution logs. Recurring entitlement processing and general cleanup create further background work, so a Cron record does not prove that all data processing finished. If no entitlements are due, the expected business change may not occur. Verify with a test case that is actually due.
Opening a page locally does not prove that the scheduled entry point works. Test the scheduled flow separately, and use isolated data to verify cleanup tasks that delete records.
6. Analytics has no data
Check the browser, collection endpoint, queue, analytics database, and reports in that order:
- Is
deploy.analytics.enabledon, and does the page load the analytics script? - Is the current path excluded?
/dashboardis excluded by default, so do not test collection by repeatedly refreshing dashboard pages. - Is Do Not Track enabled, or is a browser extension or site consent policy preventing the script from running?
- Is the collection request sent, and do its host, endpoint, and site identifier match the current configuration?
- Does the collection endpoint accept this domain, path, and origin? Do logs record a filtering reason?
- Is
ANALYTICS_QUEUEbeing consumed correctly, and have migrations been applied toANALYTICS_DB? - Do the report's site, date, and filters include this visit?
Start by checking the event count in the analytics database to avoid querying unnecessary visitor properties:
SELECT COUNT(*) AS event_count FROM analytics_event;Compare the count before and after the test operation. Do not insert fabricated visit records just to make numbers appear in the reports.
When first-party analytics is disabled, the current implementation acknowledges and discards messages already in the analytics queue. It does not automatically save them for when analytics is re-enabled. Understand the impact on existing data before changing the site ID, retention policy, or collection configuration.
Verify the fix
Start a new task that you can trace and check its messages, execution records, and final result. Then test invalid inputs or duplicate messages to confirm that they do not cause duplicate sends, charges, or entitlement grants. For analytics issues, use a page that is not excluded and trace one complete collection flow.
See Domain events and commands for Event and Command definitions and Background jobs for a fuller explanation of the design.