Azure Functions’ timer trigger is a bit of a misnomer; it’s not a simple cron job you can just schedule and forget.

Let’s see it in action. Imagine you have a function ProcessDailyReport that needs to run every day at 2 AM UTC.

{
  "scriptFile": "index.js",
  "entryPoint": "ProcessDailyReport",
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 2 * * *"
    }
  ]
}

This schedule property uses a North American cron syntax (seconds, minutes, hour, day of month, month, day of week). So, 0 0 2 * * * means at second 0, minute 0, hour 2, any day of the month, any month, any day of the week.

The problem this solves is running code on a recurring schedule without needing a separate orchestration service like Azure Logic Apps or a dedicated VM running cron. It’s built directly into the serverless compute model. Internally, the Azure Functions host monitors the timer trigger. When the schedule expression matches the current UTC time, the host invokes the function. The host itself is responsible for checking the time and triggering the function; there’s no external cron daemon involved.

The key levers you control are the schedule expression and the runOnStartup property. runOnStartup is a boolean that, when true, will execute the function immediately when the Function App starts up or restarts, in addition to its scheduled times. This is super useful for ensuring a task runs if the app has been down for a while.

{
  "scriptFile": "index.js",
  "entryPoint": "ProcessDailyReport",
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 2 * * *",
      "runOnStartup": true
    }
  ]
}

This setup means ProcessDailyReport will run at 2 AM UTC daily, and it will also run as soon as the Function App starts.

The schedule expression itself is where most of the nuance lies. It’s not just about setting a time; it’s about understanding how the Azure Functions host interprets it across potentially multiple instances of your Function App. If you set a schedule like 0 0 2 * * * to run a task that takes 30 minutes, and your Function App scales out to two instances, you might end up running the task twice within that minute if both instances evaluate the timer trigger simultaneously. The system doesn’t inherently prevent duplicate executions across scaled-out instances for a single timer tick.

The runOnStartup property, while convenient, can also be a source of unexpected behavior if you have long-running tasks. If your Function App restarts frequently (e.g., due to deployments or scaling events), a runOnStartup: true setting can cause your function to execute many times in quick succession, potentially overloading downstream systems or exceeding execution quotas. It’s crucial to ensure your function is idempotent if you enable runOnStartup.

If you need more precise control over scheduling, especially for tasks that must run only once at a specific interval regardless of scaling, or if you need to handle complex recurrence patterns (like "the last Friday of every month"), you’ll typically look at Azure Logic Apps with their built-in scheduling capabilities or integrate with Azure Durable Functions for stateful workflows. The timer trigger is best suited for relatively simple, fire-and-forget or idempotent tasks where duplicate executions, while undesirable, are not catastrophic.

The next thing you’ll likely wrestle with is how to handle long-running timer-triggered functions gracefully when your Function App scales.

Want structured learning?

Take the full Azure-functions course →