Remigiusz ZalewskiRemigiusz Zalewski

Azure Functions with .NET - Timer triggered

azure-functions-dotnet-timer-trigger

Introduction

Not every scheduled job needs a dedicated host running around the clock waiting for its next trigger - a nightly report, a cleanup task, a periodic sync can run as an Azure Function that spins up, does its work, and disappears, billed only for the seconds it actually executes. This video builds a timer-triggered Azure Function on .NET and covers the scheduling model that's easy to get subtly wrong the first time.

🎬 Watch the full video here:


The trigger is a CRON expression, with an extra field

Azure's TimerTrigger uses a six-field CRON expression - seconds first, unlike the five-field Unix CRON most people already know:

[Function("DailyCleanup")]
public void Run([TimerTrigger("0 0 2 * * *")] TimerInfo timer)
{
    // runs every day at 02:00
}

0 0 2 * * * reads as: second 0, minute 0, hour 2, every day, every month, every day of week. Copy-pasting a five-field Unix CRON expression straight in without adding the leading seconds field is the single most common mistake here - it either throws at startup or silently schedules the wrong thing.

Isolated worker model vs. in-process

Current .NET Azure Functions run on the isolated worker model - the function runs in its own process, separate from the Functions host, communicating over gRPC. This matters practically because it means the function can target any current .NET version independently of whatever runtime the Functions host itself uses, and dependency injection works exactly like a normal ASP.NET Core-style Program.cs:

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services => services.AddSingleton<ICleanupService, CleanupService>())
    .Build();
host.Run();

What happens if the function is still running at the next trigger

By default, Azure Functions does not wait for a slow execution to finish before firing the timer again - if DailyCleanup somehow takes longer than the interval between triggers, you can end up with overlapping executions. For anything where that's unacceptable (the same job stepping on itself twice), guard against it explicitly - either a distributed lock, or checking a "last run" timestamp at the start of the function before doing any work.

Local development uses a real timer too

The Azure Functions Core Tools run the same TimerTrigger locally against local.settings.json, on the actual schedule specified - there's no special "trigger it now" button by default, which makes iterating on a once-a-day schedule locally painful unless you temporarily tighten the CRON expression to something like every minute while developing.

Common pitfalls

  • Forgetting the leading seconds field, since most CRON references online use the five-field Unix format.
  • Assuming the runtime prevents overlapping executions. It doesn't by default - guard against it yourself if it matters.
  • Developing against a once-a-day schedule without temporarily tightening it, making local iteration painfully slow.
  • Not handling exceptions inside the function body. An unhandled exception fails that run silently unless you're watching Application Insights.
  • Confusing the isolated worker model's Program.cs DI setup with the older in-process model's conventions, which configure services differently.

Key Takeaways

  • Azure's TimerTrigger CRON expressions have six fields, starting with seconds - not the five-field Unix format.
  • The isolated worker model runs the function in its own process, with Program.cs-style DI independent of the Functions host's own runtime.
  • Overlapping executions aren't prevented automatically - add your own guard if a slow run could still be executing when the next trigger fires.
  • Tighten the schedule temporarily during local development rather than waiting real-time for a daily trigger.

Get the Full Source Code

The complete runnable solution - the isolated-worker Function App with the timer trigger and its DI setup - is available to Patreon supporters. If you want the scheduling and DI wiring ready to adapt to your own job instead of rebuilding it from the walkthrough above, you can find it on Patreon.

Resources