The great thing about “serverless” code is that you don’t need to worry about servers at all. If my function gets invoked 10 times, all 10 invocations might run on the same server, or they might run on 10 different servers. I don’t need to know or care.

But suppose every time my function runs I need to look something up in a database. I might decide that it would be nice to temporarily cache the response in memory so that subsequent runs of my function can run a bit faster (assuming they run on the same server as the previous invocation).

Is that possible in Azure Functions? I did a bit of experimenting to see how it could be done.

To keep things simple, I decided to make a C# webhook function that counted how many times it had been called. And I counted in four ways. First, using a static int variable. Second, using the default MemoryCache. Third, using a text file in the home directory. Fourth, using a per-machine text file in the home directory. Let’s see what happens with each of these methods.

1. Static Integer

If you declare a static variable in your run.csx file, then the contents of that variable are available to all invocations of your function running on the same server. So if our function looks like this:

static int invocationCount = 0;

public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
    log.Info($"Webhook triggered {++invocationCount}");
return ... }

And we call it a few times, then we’ll see the invocation count steadily rising. Obviously this code is not thread-safe, but it shows that the memory persists between invocations on the same server.

Unsurprisingly, every time you edit your function, the count will reset. But also you’ll notice it reset at other times too. There’s no guarantee that what you store in a static variable will be present on the next invocation. But it’s absolutely fine for temporarily caching something to speed up function execution.

2. MemoryCache

The next thing I wanted to try was sharing memory between two different functions in the same function app. This would allow you to share a cache between functions. To try this out I decided to use MemoryCache.Default.

static MemoryCache memoryCache = MemoryCache.Default;
public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
    var cacheObject = memoryCache["cachedCount"];
    var cachedCount = (cacheObject == null) ? 0 : (int)cacheObject;   
    memoryCache.Set("cachedCount", ++cachedCount, DateTimeOffset.Now.AddMinutes(5));

    log.Info($"Webhook triggered memory count {cachedCount}");
    return ...

Here we try to find the count in the cache, increment it, and save it with a five minute expiry. If we copy this same code to two functions within the same Azure Function App, then sure enough they each can see the count set by the other one.

Again, this cache will lose its contents every time you edit your code, but its nice to know you can share in-memory data between two functions running on the same server.

3. On Disk Shared Across All Servers

Azure function apps have a %HOME% directory on disk which is actually a network share. If we write something into that folder, then all instances of my functions, whatever server they are running on can access it. Let’s put a text file in there containing the invocation count. Here’s a simple helper method I made to do that:

private static int IncrementInvocationCountFile(string fileName)
    var folder = Environment.ExpandEnvironmentVariables(@"%HOME%\data\MyFunctionAppData");
    var fullPath = Path.Combine(folder, fileName);
    Directory.CreateDirectory(folder); // noop if it already exists
    var persistedCount = 0;
    if (File.Exists(fullPath))
        persistedCount = int.Parse(File.ReadAllText(fullPath));
    File.WriteAllText(fullPath, (++persistedCount).ToString());
    return persistedCount;

We can call it like this:

public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
    var persistedCount = IncrementInvocationCountFile("invocations.txt");
    log.Info($"Webhook triggered {persistedCount}");
    return ...;

Obviously this too isn’t thread-safe as we can’t have multiple instances of our function reading and writing the same file, but the key here is that anything in this folder is visible to all instances of our function, even across different servers (although it was several days before I saw my test function actually run on a different server). And unlike the in memory counter, this won’t be lost if your function restarts for any reason.

4. Per Machine File

What if you want to use disk storage for temporary caching, but only want per machine? Well, each server does have a local disk, and you can write data there by writing to the %TEMP% folder. This would give you temporary storage that persisted on the same server between invocations of functions in the same function app. But unlike putting things in %HOME% which the Azure Functions framework won’t delete, things you put in %TEMP% should be thought of as transient. The temp folder would probably best be used for storing data needed during a single function invocation.

For my experiment I decided to use System.Environment.MachineName as part of the filename, so each server would maintain its own invocation count file in the %HOME% folder.

public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
    var machineCount = IncrementInvocationCountFile(System.Environment.MachineName + ".txt");
    log.Info($"Webhook triggered {machineCount}");
    return ...;

And so now I can use Kudu to look in my data folder and see how many different machines my function has run on.

Should I do this?

So you can use disk or memory to share state between Azure Functions. But does that mean you should?

Well, first of all you must consider thread safety. Multiple instances of your function could be running at the same time, so if you used the techniques above you’d get file access exceptions, and you’d need to protect the static int variable from multiple access (the MemoryCache example is already thread-safe).

And secondly, be aware of the limitations. Anything stored in memory can be lost at any time. So only use it for temporarily caching things to improve performance. By contrast anything stored in the %HOME% folder will persist across invocations, restarts and different servers. But it’s a network share. You’re not really storing it “locally”, so it’s not all that different from just putting the data you want to share in blob storage or a database.

Vote on HN
comments powered by Disqus