Beware of TimerTrigger in Azure Functions

TimerTrigger is one of the easiest triggers in Azure Functions to handle. It has however an interesting "issue", which is described in the documentation - you should not use the same id property for different Function App. Why is that?

Just as planned

Let's assume you have two identical functions triggered using TimerTrigger:

public static void Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, TraceWriter log)
	log.Info($"TIMER trigger function executed at: {DateTime.Now}");

Assuming our host.json file is empty, after we deploy two Function Apps, we'll get following results:

As you can see both are working correctly(I changed displayed text a little bit so you can see the difference). Now let's abuse things a little - I forced two Function Apps to work under the same id. To do so I changed my host.json file:

  "id": "9f4ea53c5136457d883d685e57164f08"

With this change something broke:

As we can read in documentation:

If you share a Storage account across multiple function apps, make sure that each function app has a different id in host.json. You can omit the id property or manually set each function app's id to a different value. The timer trigger uses a storage lock to ensure that there will be only one timer instance when a function app scales out to multiple instances. If two function apps share the same id and each uses a timer trigger, only one timer will run.

What is the cause of such behaviour?

Underlying functionality

If you check the underlying storage powering Azure Function, you'll find azure-webjobs-host container, which stores e.g. locks from TimerTrigger. Mine looks like this:

As you can see I have 3 different locks - timertriggertest come from the previous version of my functionality, where I didn't have id explicitely set. The very first folder is named using id property in host.json file.

We can find also in the source code. There's a class PrimaryHostCoordinator, which is used to determine which host instance is the primary one. It is initialized at the very beginning after functions has been loaded and tries to aquire a lock on a blob every 5 seconds. If it fails, a function won't be triggered. Remember not to set id if you don't have to and if you do, try to avoid using functions tiggered by TimerTrigger in different Function Apps.

Don't "admin" me!

This was somehow unexpected. I was flawlessly developing my API using Azure Functions and then BANG! - suddenly my functions are in error state. Just like that - no stack trace, no detailed explanation. I took me some time to realize, that they all have one thing in common - they share the admin part. Apparently Functions have some problems when you're using HttpTrigger with a custom route containg admin word. What is the reason?

Check the codebase

Let's take a quick look at the codebase of Functions. Finding a place, which causes the error is pretty simple:

internal static void ValidateHttpFunction(string functionName, HttpTriggerAttribute httpTrigger, bool isProxy = false)
	if (string.IsNullOrWhiteSpace(httpTrigger.Route) && !isProxy)
		// if no explicit route is provided, default to the function name
		httpTrigger.Route = functionName;

	// disallow custom routes in our own reserved route space
	string httpRoute = httpTrigger.Route.Trim('/').ToLowerInvariant();
	if (httpRoute.StartsWith("admin"))
		throw new InvalidOperationException("The specified route conflicts with one or more built in routes.");

As you can see, all routes which start with admin are disallowed. While this is perfectly fine, I couldn't find any mention in the documentation, which would clarify this. 

When in doubt always try to check the source. Some answers will be quite difficult to obtain, but you'll end up with a much better overall understanding of the library. The rest will be rather straightforward, nothing you can't handle, isn't it?