Azure Function Apps and Durable Functions, Part 2: Best Practices and Lessons Learnt

Daniel Ward
Mesh-AI Technology & Engineering
10 min readFeb 20, 2024

Hopefully you already read Part 1 covering what Function Apps and Durable Apps are, so if you need an introduction to the functionality of these tools and a comparison between them, start there.

This article will focus more on working with Azure Functions and, in particular, Durable Functions. We’re going to dive into each Best Practice item and Lesson Learnt one by one. I should emphasise that this is written with Python in mind, but the principles will apply to most languages. You may nonetheless find some specific references to Python packages or libraries, or challenges in working with the Python runtime for Azure Functions.

I suggest checking Part 1 to understand more about the structure of Azure Function Apps and how they operate, especially for Durable Functions.

How to handle the documentation

While this may change over time, the biggest pain point of working with Azure Functions (at this point in time) and especially Durable Functions is that the official documentation at the moment of writing is not complete or easy to use.

Community-written articles and topics covering Durable Functions are particularly thin; most non-Microsoft sources I found used obvious excerpts from the Microsoft documentation without expanding any further on why certain components were as they were (looking at you, context_name = "context" and winner = yield context.task_any()). You will not find many useful StackOverflow topics, and GPT knowledge on Durable Functions in particular is equally slim.

The single best recommendation I can make here is to reference the official GitHub examples (Python samples here). If you are using Python, you will find the v1 model has more samples than the v2. As far as I’m aware, all v1 functionality is available in v2; the samples have just not been made for it. Use these as starters / boiler-plate code from which to build your code out from.

✳️ I was able to get a Slackbot to be powered by Durable Functions running the Python Programming Model V2, allowing the bot to send messages, wait on a response, then continue running. Human Interaction is not a sample provided for v2 at the time of writing, but I can confirm it’s entirely possible.

Understand that Trigger Functions, Orchestration Functions, and Activity Functions “aren’t that special”

One of the headaches I made for myself starting out was understanding what Trigger, Orchestration, and Activity Functions could do. From the documentation, it appears that Trigger Functions cannot do much more than kick off or resume an Orchestration Function, and Orchestration Functions shouldn’t be used for more than organising your Activity Functions and co-ordinating those whilst maintaining state.

Here’s the spoiler: You can do mostly whatever you want in each of the three functions. If you really want to implement some (potentially wildly complicated) logic to handle a Trigger Function in order to decide which orchestrator function to kick-off, you can do that. If you want to do all your processing in your Orchestrator Function, you can do that. There are very small, but very strict, boundaries on what each function can do:

  1. Trigger Functions can be triggered by external events. Other function types cannot.
  2. Orchestrator Functions can be paused. Other function types cannot.

That’s it (as far as I’m aware). So as long as you have externally triggered Trigger Functions kicking off your Orchestrator Function(s), which may or may not need to pause to maintain state, you can pretty much do whatever you want anywhere.

If your app might use Durable Functions in the future, start using it immediately

Durable Functions can perform as regular (non-durable) Azure Functions. Getting the boilerplate in place to begin with is a relatively low effort task at the start, but more time-consuming to do later. At its most basic, you can use just a Trigger Function and have that kick off an Orchestrator Function (see the section below), which is not much more complicated than a regular Azure Function but allows easy transition to Durable Functions later.

You might not need Activity Functions

These are most useful when running parallel compute in Durable Functions. If you’re not doing that, then Activity Functions might not be worth the effort.

The big headache you gain from using Activity Functions is that they are spun up as separate instances, running in their own Azure Function. That means:

  • They have their own log traces.
  • They invoke another instance and more CPU minutes (although, you are not charged for an Orchestration Function waiting on external input, see Pattern 5’s note here)
  • Any information must be passed back to the orchestrator in the form of a JSON object.

If you will only have a single thread running, don’t complicate it. Keep it as part of your Orchestration Function. However, if you think there is a chance you might need to make the given functionality run in parallel, it’s better to set up with Activity Functions in the first instance than having to remediate your code for it later.

✳️ If you’re thinking that Activity Functions would help keep your code well structured, just know you can still use Classes and Functions within your Orchestrator Functions like you would with any other code.

Add logging and log everything

Obviously, logging is always a good thing to do, but one of the major frustrations I found is when identifying logic errors in Azure Functions. When your request to your Function App gets a 500 Internal Server Error response, you can (usually) rest easy knowing that an error has been generated at the point of failure and logged in AppInsights for you to investigate. What is considerably more frustrating is when you get 200 OK response, but what you expected to happen, didn’t happen.

Extensive logging will help with this. Azure Functions natively supports the Python Logging library, so I recommend going slightly crazy and adding logging.info("PYLOG: [Variable] has content [content] at line [#]") everywhere.

⚠️ You don’t have to start it with an identifier, like PYLOG, but this does mean you can filter AppInsights on PYLOG to cut out all the other gibberish and focus just on your code. You can, of course, use whatever you want to signify this. You might even want to replace this with the function name to hone in on the particular piece you’re currently developing.

Consider building your own serverless solution instead of using Durable Functions

As discussed in Part 1, building your own stateful serverless solution (instead of using Azure Functions) is not too hard and will cut out a lot of the challenges of working with the documentation. It will work exactly as you design it to work and the functionality available is that which you give it. I cannot stress enough that understanding the functions and operations available to you through the Azure Functions packages and the Azure Documentation is challenging and frequently unclear.

✳️ A benefit of coding your own serverless solution is you can get avoid some Azure Functions limitations as well. The main one that springs to mind is, by storing state in a Key-Value store and waiting for it to be recalled, you can have an application that effectively never times out. Of course, this does also mean you have to code something in to handle stale states later on…

If using Python, consider sticking with the v1 programming model

There is no difference in functionality between the v1 and v2 Python programming language models. What changes is how you implement the functionality and the structure of files in your Python Azure Function. By far the biggest difference is that the v2 model relies extremely heavily on decorator functions. Whilst this provides a long-term benefit of making it easier to set up new functions and interact with them, it also makes it harder to understand exactly what is going on in your code.

If you are likely to be using Azure Functions very frequently and in many different scenarios, the v2 model is likely worth learning. However, having spoken with colleagues about this, the v1 model is generally easier to learn and use, but comes with the addition of some extra boilerplate code / files.

⚠️ There’s no guarantee, but it is also a fair assumption that Azure will stop supporting the v1 version at some point. However, I’ve not heard anything about this yet, and all functionality appears to be available across both models. Fundamentally, they use the same azure-functions Python package, so they may forever maintain both.

If coding Python Functions, use VSCode and Azurite if possible

One of the pains of building serverless functions is that you just don’t know if your triggers and code as a whole will work until you deploy them. Fortunately, Azure provides a solution for this, allowing you to host and trigger your serverless function locally. This will save you considerable time instead of having to deploy, run, wait for the logs to update, check the logs, find what went wrong, make a correction, then repeat. Getting this set up can take a bit of time, but I would wager it will save you considerable time in the long-run.

✳️ If you have tried to work with Azure Functions in the past whilst using an M1 Mac, you may remember that there was a bug that stopped you from using this locally. Fortunately, that appears to be fixed.

What to do if your functions aren’t registering

Deploying a function and finding out it hasn’t registered can be a massive headache, as Azure will give you no feedback on what went wrong. Using VSCode and Azurite (as in the above section) should help with this, but it’s still worth knowing what to look for when you deploy your function and it reports that there are no functions deployed.

  • Check for syntax errors. If a function contains one, it won’t register.
  • Check your requirements.txt. Did you add a new package to your code and forget to update your requirements.txt? If so, the deployment will recognise your functions due to missing package requirements. Just run a pip freeze > requirements.txt whenever you add a new import and you’ll be fine (although, you may want to clean your environment to just used packages in order to minimise the size of your function deployments!)
  • Check your Functions have the correct boilerplate code / decorators. This goes as far as checking that you reference your context, client_name, or input_name correctly in both the decorator / boilerplate, and in the rest of the code.
This v2 model code will fail, as the Orchestrator is referencing “context” when it should reference “context2” (or vice versa).
  • Double check your project structure. Only worth checking if you’re hitting this error on your first deployment. Are you using the v2 Decorators but setup for a v1 project? Check the docs for the right setup.
  • Check your Function App configuration settings. Incorrect or missing settings can result in your Functions not registering. Finding the right setting for your Function is unfortunately not a pleasant experience: You can see the full list of possible options here.
  • Redeploy your App Function resource. This is the nuclear option. Sometimes, the files in your App Function can reach a state where they are not fully cleared from the Function when you deploy your new iteration. That can result in your function never deploying successfully and your functions not updating (or, showing as unregistered). You can fix this manually by going into your App Function files and clearing out the old files, but if you’ve set up Infrastructure as Code for your deployment and you can rapidly redeploy all your functions, this is a simpler option that takes considerably less time and effort.

For Durable Functions and Apps that trigger Apps, plan what data is shared between functions

As Azure Functions spin up each function in their own instance, you will need to pass data between them when they are invoked. You will typically do this through HTTP POST Requests with a JSON payload attached. This means you need to:

  1. Recognise what data you want to share from one function to another.
  2. Process that data into a JSON payload, and send the request.
  3. Process reading that data on the other side.

For one Azure Function calling another, this is not too much of a headache.

With Durable Functions, it can be slightly more confusing as you are not sending a standard request, but rather invoking another Function through an existing Function, all within the “same” Durable Function. What that practically means is: Rather than using something like the requests library to send your request, you’re using Azure Durable Functions Python package to send the request and then (usually) collect a response.

This is a snippet from an Orchestration Function. Line 1 here gets the input to the Orchestration Function. Line 2 gets the Instance ID for the current Orchestration and adds it to the input payload (see explanation below). Line 3 sets off an Activity Function referenced in the same code directly, rather than by POST request.

Above all else, remember that to call an Orchestration Function waiting on external input, you will need to use its Instance ID, so this information must be collected and passed through to whatever will eventually send information to the Orchestration Function.

✳️ It does occur to me now in writing this that there probably isn’t anything stopping you from ignoring the call_activity job above and instead running a POST request if you really want to. However, this would mean you have to set up a HTTP trigger for the activity function, instead of just calling it directly. Ultimately, it is simpler, and likely cheaper and more secure, to just use the call_activity function.

Closing Thoughts

I largely covered my summary of thoughts in Part 1 but, overall, I believe while they can be frustrating at times, Azure Functions and Durable Functions are useful. Durable Functions in particular can be painful, and I understand why many people still recommend using Logic Apps if you need a stateful process rather than Durable Functions.

I would also like to quickly say, hello reader! If you’re here, you’re likely (certainly?) looking for some insight on using Azure Functions / Durable Functions. As I mentioned above, the documentation is relatively poor and there aren’t that many community discussions on the topic. So, I encourage that you post any Functions tips and tricks you learn about into the comments on this post to help others who end up here as well. Happy coding!

--

--