Automation
Azure Functions: diagnose a Timer Trigger before replaying a job
A production runbook for qualifying a failed Azure Functions Timer Trigger with execution history, schedule locks, storage, Application Insights, idempotency, validation and rollback before manual replay.
An Azure Functions Timer Trigger often looks simple: a CRON expression, a function, coordination storage and a scheduled workload. In production, risk appears when the job does not run, runs late, overlaps with itself or fails after a partial action. Manual replay feels like the fastest move. It can also duplicate work, replay already-consumed items or hide a runtime failure.
The use case is a scheduled job that reconciles data, purges expired objects, starts a synchronization, calls a business API or prepares a billing batch. The team sees a missing outcome or an error in Application Insights. Before clicking “Run”, redeploying the Function App or changing the schedule, the team must qualify three things: did the trigger really miss its window, is the workload idempotent, and which replay is safe.
Name the replay decision
Start by naming the decision you need. The diagnostic is not only about explaining an error. It must choose between waiting, replaying, fixing configuration, replaying a subset or rolling back the latest change.
Decision to produce
Do not replay and wait for the next schedule
Replay manually with a bounded scope
Replay only unprocessed items
Fix configuration or identity before replay
Temporarily disable the trigger
Roll back the latest application or schedule change
Minimum evidence
Function App, function, slot and region
Expected CRON expression and effective timezone
Last successful execution
Failed or missing execution
Runtime state and AzureWebJobsStorage state
Idempotency trace or deduplication key
Effects already produced by the failed execution
Known rollback or disablement command Without this contract, the discussion quickly turns into “let’s replay and see”. In operations, a replay is a production action. It needs a scope, an owner and a return path.
Check whether the trigger really missed its window
A job may look absent because the view is filtered, ingestion is delayed or the function started but failed before the first business log. Rebuild the timeline from Application Insights first.
let startTime = datetime(2026-07-23T00:00:00Z);
let endTime = datetime(2026-07-23T08:00:00Z);
let functionName = "NightlyReconciliation";
traces
| where timestamp between (startTime .. endTime)
| where cloud_RoleName has "func-prod-billing"
| where message has functionName or tostring(customDimensions["Category"]) has functionName
| project timestamp,
operation_Id,
severityLevel,
message,
InvocationId = tostring(customDimensions["InvocationId"]),
Category = tostring(customDimensions["Category"])
| order by timestamp asc If no trace appears, do not conclude too early that the trigger was silent. Check exceptions, internal requests and ingestion latency as well. An observability issue can look like a missing execution.
Check runtime, scale and coordination storage
The Timer Trigger depends on the Functions runtime and the storage account used for locks, leases and schedule state. A failing AzureWebJobsStorage, network restriction, key rotation or broken identity can prevent coordination even when the business code is fine.
Runtime checks
Function App started and in the expected slot
Runtime version compatible with the deployed code
WEBSITE_RUN_FROM_PACKAGE or deployed package readable
Schedule settings identical between slot and production
Always On enabled when the hosting plan requires it
Storage checks
AzureWebJobsStorage resolves to the expected account
Network access allowed from the Function App
Secret or identity still valid
Blob leases available
No throttling or 403 on the storage account
No recent firewall, DNS or Key Vault reference change This block prevents a fix at the wrong layer. If coordination storage is unavailable, manual replay does not repair the cause. It may only create an off-schedule execution in addition to the next automatic run.
Treat the failure as a partial action
A Timer Trigger that fails after three minutes is not the same as a trigger that never started. It may already have written rows, sent notifications, moved files or committed an external step. Treat the failure as a partial action until the evidence says otherwise.
let invocationId = "00000000-0000-0000-0000-000000000000";
union traces, exceptions, dependencies
| where tostring(customDimensions["InvocationId"]) == invocationId
or operation_Id == invocationId
| extend dependencyTarget = column_ifexists("target", "")
| extend dependencyResult = column_ifexists("resultCode", "")
| project timestamp,
itemType,
severityLevel,
message,
dependencyTarget,
dependencyResult,
success = column_ifexists("success", bool(null))
| order by timestamp asc Look for progress markers: batch opened, item processed, checkpoint written, external call completed, message published, lock released. The safe replay path depends less on the final error and more on what already happened.
Prove idempotency before replay
The central question is simple: what happens if the same input is processed twice? If the answer is vague, the replay must be bounded or replaced by a targeted manual correction.
timer_job:
function: NightlyReconciliation
schedule: "0 30 2 * * *"
business_window: previous_day
idempotency:
replay_key: billing_date + customer_id
checkpoint_store: reconciliation_runs
duplicate_policy: skip_if_completed
external_calls:
invoice_api: requires_idempotency_key
notification_api: send_only_after_commit
manual_replay_allowed_when:
- failed_invocation_identified
- completed_items_exported
- pending_items_query_reviewed
- duplicate_policy_tested
- rollback_owner_available A genuinely idempotent job leaves deduplication evidence. A job that relies on “it should be fine” is not idempotent from an operations point of view.
Prepare a bounded replay
A safe replay avoids rerunning everything by default. Prefer an explicit scope: time window, business identifiers, incomplete batch, dry-run mode or resume parameter. If the code exposes no boundary, that is an operational gap to fix before expanding the job’s use.
FUNCTION_APP="func-prod-billing"
FUNCTION_NAME="NightlyReconciliation"
RESOURCE_GROUP="rg-prod-automation"
# Example bounded replay: adapt parameters to the real function contract.
az functionapp function invoke --resource-group "$RESOURCE_GROUP" --name "$FUNCTION_APP" --function-name "$FUNCTION_NAME" --data '{"mode":"replay","businessDate":"2026-07-22","dryRun":true,"maxItems":50}' The first replay should ideally be a dry run. If that mode does not exist, at least validate the list of items to resume and the duplicate policy before the real execution.
Watch the replay and the next schedule
Manual replay does not close the incident. You still need to validate the replay outcome and observe the next automatic trigger. Many issues return at the next schedule because the root cause was configuration, not the failed instance.
let replayStart = datetime(2026-07-23T08:10:00Z);
let watchUntil = datetime(2026-07-23T10:00:00Z);
traces
| where timestamp between (replayStart .. watchUntil)
| where cloud_RoleName has "func-prod-billing"
| where message has "NightlyReconciliation"
| summarize events=count(),
errors=countif(severityLevel >= 3),
sampleError=anyif(message, severityLevel >= 3)
by bin(timestamp, 5m), operation_Id
| order by timestamp asc Add business validation outside logs: number of processed objects, absence of duplicates, checkpoint state, downstream metric and change ticket. Logs prove that the function ran; they do not always prove that the business effect is correct.
Decide fix, rollback or contract change
The runbook output should be explicit. If the replay succeeds but the cause remains unknown, keep the incident open for observation. If a recent change caused the issue, roll back the smallest possible surface: configuration, secret, Key Vault reference, storage firewall, slot, package or CRON expression.
Replay accepted
Failure qualified
Partial effects identified
Items to replay listed
Idempotency or deduplication proven
Dry run or limited replay validated
Next schedule monitored
Rollback
Runtime can no longer coordinate the timer
Coordination storage is unreachable
New CRON expression fires outside the business window
Replay creates duplicates
Partial effects cannot be reconciled
Change the contract
Add dry-run mode
Add a business replay_key
Write one checkpoint per item
Expose a bounded resume parameter
Log InvocationId, business window and item counters A clean rollback may be as simple as restoring the previous CRON expression or reverting a Key Vault reference. The point is to reduce risk before replay, not after it.
Conclusion
A Timer Trigger is not just a clock. It is production automation with coordination, state, business effects and replay windows. When it fails, the right reaction is not an immediate replay. It is to qualify the timeline, runtime, storage, partial action and idempotency.
With this runbook, the team can decide whether to wait for the next schedule, replay a bounded scope, fix configuration, roll back a change or improve the job contract. Replay becomes an operable decision, not a bet on a button.