A finance team called me on a Monday morning because their invoice export had not run. The job was supposed to fire at 6:00 a.m. Eastern, every weekday. The developer had copied a five-field Linux cron line into the function, and the app failed to start on deploy. When he fixed it, the job ran at 1:00 a.m. Eastern instead, because nobody had set a time zone.
Both problems come down to one thing: the Azure Functions CRON expression works differently from the cron you know from Linux. It has six fields, it defaults to UTC, and it behaves in specific ways when the app scales out or restarts. I have fixed these issues for reporting jobs, file cleanups, and nightly data syncs.
By the end of this guide, you will write correct schedules, configure time zones, deploy a timer-triggered function securely, and troubleshoot missed or duplicate runs.
What an Azure Functions CRON Expression Does
A timer trigger runs a function on a schedule. The schedule is defined with an NCRONTAB expression, which is the cron-style format that Azure Functions uses. If you are new to the service, start with this overview of what Azure Functions is, and see how to trigger Azure Functions to compare timer triggers with HTTP and queue triggers.
The expression has six fields, separated by spaces:
text{second} {minute} {hour} {day} {month} {day-of-week}The extra field is the first one. Standard Linux cron has no seconds field, so a five-field line fails or shifts meaning. Here is what each field accepts:
| Field | Allowed values | Notes |
|---|---|---|
| Second | 0–59 | Most jobs use 0 |
| Minute | 0–59 | |
| Hour | 0–23 | 24-hour clock |
| Day | 1–31 | Day of month |
| Month | 1–12 | Or names such as JAN |
| Day of week | 0–6 | 0 is Sunday; names such as MON also work |
Special characters change how a field behaves. The asterisk * means every value. A comma lists values, such as 1,15. A hyphen sets a range, such as 1-5. A slash sets an interval, such as */10 for every 10 units.
Pro Tip: In my experience, I write the schedule in plain English in a code comment right above the expression. When a teammate reads
0 30 6 * * 1-5six months later, they should not need to decode it.
Common Schedule Examples You Can Copy
These examples cover most business needs. Test each one before you trust it.
| Requirement | Expression |
|---|---|
| Every 5 minutes | 0 */5 * * * * |
| Every hour, on the hour | 0 0 * * * * |
| Daily at 2:00 a.m. | 0 0 2 * * * |
| Weekdays at 6:30 a.m. | 0 30 6 * * 1-5 |
| First day of each month at midnight | 0 0 0 1 * * |
| Every 15 seconds | */15 * * * * * |
| Sundays at 11:45 p.m. | 0 45 23 * * 0 |
| Twice daily at 8:00 a.m. and 5:00 p.m. | 0 0 8,17 * * * |
Notice the leading 0 in most of them. If you write * */5 * * * *, the first field means every second, so the function fires every second during each matching minute. That mistake can create thousands of executions, and on a Consumption plan, it creates a real bill.
There is no built-in “last day of the month” token. For month-end jobs, run the function daily and check the date in code. This is a simple pattern, and it avoids brittle schedules.
Pro Tip: I once saw
* */10 * * * *deployed to production by mistake. It ran 60 times in each matching minute, and the downstream API throttled us within an hour. I now review every schedule line in pull requests.
Choose the Right Hosting Plan First
The plan you pick changes cost, cold starts, and reliability for timers. The Consumption plan is serverless. You pay per execution and compute time, and the app scales to zero. The Premium plan keeps pre-warmed instances ready and supports private networking. The Dedicated (App Service) plan runs on always-on virtual machines you already pay for.
Compare them in this guide to the Azure Functions Consumption plan vs. Premium, and read how the Premium plan works if you need virtual network access.
Here is how I decide:
- Infrequent jobs, such as a nightly report: Consumption is usually cheapest because you pay only while the job runs.
- Jobs that need to reach a private database: Premium, because it supports virtual network integration.
- Many jobs on a plan that already runs 24/7: Dedicated, since the compute is already paid for.
Timeouts matter for scheduled work. A Consumption plan has a default execution timeout of 5 minutes, and the maximum is 10 minutes. A long data sync will fail silently if you ignore this. Read about the Azure Functions timeout and the broader Azure Functions limitations before you commit to a design. If a job needs more time, split it into smaller chunks or move to a plan with a longer timeout.
Pro Tip: I put a rough runtime budget in every design document. If a timer job takes 4 minutes on Consumption, I plan for the day it takes 12.
Build the Function Step by Step
Step 1: Create the resource group
An Azure resource group is a logical container for related resources. Create one per workload and environment so you can manage, tag, and delete things together. If you have not signed in yet, learn how Azure CLI login works.
az group create \
--name rg-invoice-jobs-prod \
--location eastus \
--tags environment=prod workload=invoice-export owner=finance-itThis command creates a resource group named rg-invoice-jobs-prod in the East US region. The --tags parameter adds labels that make cost reviews easier. Choose a region close to the systems your function calls, since cross-region calls add latency and sometimes data transfer cost. Read about Azure tags if you want a naming plan.
Step 2: Create storage and the function app
Every function app needs a storage account. Timer triggers use it to store lock and schedule status data, which is how Azure avoids running the same timer twice.
az storage account create \
--name <storage-account-name> \
--resource-group rg-invoice-jobs-prod \
--location eastus \
--sku Standard_LRS \
--allow-blob-public-access false \
--min-tls-version TLS1_2This creates a general-purpose storage account with locally redundant storage. --allow-blob-public-access false blocks anonymous blob access, and --min-tls-version TLS1_2 rejects older encryption. For more hardening ideas, see how to secure an Azure storage account.
az functionapp create \
--name <function-app-name> \
--resource-group rg-invoice-jobs-prod \
--storage-account <storage-account-name> \
--consumption-plan-location eastus \
--runtime dotnet-isolated \
--functions-version 4 \
--os-type Windows \
--assign-identity "[system]"This creates a function app on the Consumption plan using the .NET isolated worker. --functions-version 4 selects the current runtime. --assign-identity "[system]" turns on a system-assigned managed identity, an identity that Azure creates and manages so your code needs no stored password.
The az functionapp create guide explains more options. I chose Windows here because time zone support is simplest there, and the next section explains why.
Pro Tip: I tag the storage account with the same workload tag as the function app. When finance asks why storage costs rose, I can answer in one filter.
Step 3: Write the timer function
Here is a C# example using the isolated worker model. If you prefer the editor workflow, follow this tutorial on creating an Azure Functions timer trigger, or create a function in Visual Studio Code.
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
public class InvoiceExport
{
private readonly ILogger<InvoiceExport> _logger;
public InvoiceExport(ILogger<InvoiceExport> logger)
{
_logger = logger;
}
// Weekdays at 6:30 a.m. in the app's configured time zone
[Function("InvoiceExport")]
public void Run(
[TimerTrigger("%INVOICE_EXPORT_SCHEDULE%", RunOnStartup = false)] TimerInfo timer)
{
_logger.LogInformation("Invoice export started at {Time}", DateTime.UtcNow);
if (timer.IsPastDue)
{
_logger.LogWarning("Timer is past due. A scheduled run was missed.");
}
// Call your export logic here
}
}The %INVOICE_EXPORT_SCHEDULE% syntax reads the schedule from an app setting instead of hardcoding it. This lets you use a fast schedule in test and a real one in production, with no code change. RunOnStartup = false stops the function from running every time the app starts or scales.
Never set RunOnStartup to true in production, because deployments and restarts would trigger the job unexpectedly. The IsPastDue property tells you the function is running late.
For the Python v2 model, the same idea looks like this:
import logging
import azure.functions as func
app = func.FunctionApp()
@app.timer_trigger(schedule="%INVOICE_EXPORT_SCHEDULE%",
arg_name="timer",
run_on_startup=False,
use_monitor=True)
def invoice_export(timer: func.TimerRequest) -> None:
if timer.past_due:
logging.warning("Timer is past due.")
logging.info("Invoice export started.")use_monitor=True keeps schedule status in storage so missed runs are detected after a restart. See how to build with Azure Functions in Python for setup details.
Step 4: Set the schedule and time zone
Store the schedule as an app setting.
az functionapp config appsettings set \
--name <function-app-name> \
--resource-group rg-invoice-jobs-prod \
--settings "INVOICE_EXPORT_SCHEDULE=0 30 6 * * 1-5" \
"WEBSITE_TIME_ZONE=Eastern Standard Time"The --settings parameter adds two app settings. The first is the schedule, 6:30 a.m. on weekdays. The second sets the time zone, so the expression runs in Eastern time instead of UTC.
This is where my finance client got burned. By default, timer triggers use UTC, so a 6:30 schedule fires at 2:30 a.m. or 1:30 a.m. Eastern, depending on daylight saving time. On Windows, WEBSITE_TIME_ZONE uses Windows time zone names like Eastern Standard Time.
On Linux, the setting uses IANA names like America/New_York, but support varies by plan, an