Best practices for performance and reliability of Azure Functions
This article provides guidance to improve the performance and reliability of your serverless function apps.
The following are best practices in how you build and architect your serverless solutions using Azure Functions.
Avoid long running functions
Large, long-running functions can cause unexpected timeout issues. To learn more about the timeouts for a given hosting plan, see function app timeout duration.
A function can become large because of many Node.js dependencies. Importing dependencies can also cause increased load times that result in unexpected timeouts. Dependencies are loaded both explicitly and implicitly. A single module loaded by your code may load its own additional modules.
Whenever possible, refactor large functions into smaller function sets that work together and return responses fast. For example, a webhook or HTTP trigger function might require an acknowledgment response within a certain time limit; it's common for webhooks to require an immediate response. You can pass the HTTP trigger payload into a queue to be processed by a queue trigger function. This approach lets you defer the actual work and return an immediate response.
Cross function communication
If not using Durable Functions or Logic Apps to integrate with multiple functions, it's best to use storage queues for cross-function communication. The main reason is that storage queues are cheaper and much easier to provision than other storage options.
Individual messages in a storage queue are limited in size to 64 KB. If you need to pass larger messages between functions, an Azure Service Bus queue could be used to support message sizes up to 256 KB in the Standard tier, and up to 1 MB in the Premium tier.
Service Bus topics are useful if you require message filtering before processing.
Event hubs are useful to support high volume communications.
Write functions to be stateless
Functions should be stateless and idempotent if possible. Associate any required state information with your data. For example, an order being processed would likely have an associated
state member. A function could process an order based on that state while the function itself remains stateless.
Idempotent functions are especially recommended with timer triggers. For example, if you have something that absolutely must run once a day, write it so it can run anytime during the day with the same results. The function can exit when there's no work for a particular day. Also if a previous run failed to complete, the next run should pick up where it left off.
Write defensive functions
Assume your function could encounter an exception at any time. Design your functions with the ability to continue from a previous fail point during the next execution. Consider a scenario that requires the following actions:
- Query for 10,000 rows in a database.
- Create a queue message for each of those rows to process further down the line.
Depending on how complex your system is, you may have: involved downstream services behaving badly, networking outages, or quota limits reached, etc. All of these can affect your function at any time. You need to design your functions to be prepared for it.
How does your code react if a failure occurs after inserting 5,000 of those items into a queue for processing? Track items in a set that you’ve completed. Otherwise, you might insert them again next time. This double-insertion can have a serious impact on your work flow, so make your functions idempotent.
If a queue item was already processed, allow your function to be a no-op.
Take advantage of defensive measures already provided for components you use in the Azure Functions platform. For example, see Handling poison queue messages in the documentation for Azure Storage Queue triggers and bindings.
Function organization best practices
As part of your solution, you may develop and publish multiple functions. These functions are often combined into a single function app, but they can also run in separate function apps. In Premium and dedicated (App Service) hosting plans, multiple function apps can also share the same resources by running in the same plan. How you group your functions and function apps can impact the performance, scaling, configuration, deployment, and security of your overall solution. There aren't rules that apply to every scenario, so consider the information in this section when planning and developing your functions.
Organize functions for performance and scaling
Each function that you create has a memory footprint. While this footprint is usually small, having too many functions within a function app can lead to slower startup of your app on new instances. It also means that the overall memory usage of your function app might be higher. It's hard to say how many functions should be in a single app, which depends on your particular workload. However, if your function stores a lot of data in memory, consider having fewer functions in a single app.
If you run multiple function apps in a single Premium plan or dedicated (App Service) plan, these apps are all scaled together. If you have one function app that has a much higher memory requirement than the others, it uses a disproportionate amount of memory resources on each instance to which the app is deployed. Because this could leave less memory available for the other apps on each instance, you might want to run a high-memory-using function app like this in its own separate hosting plan.
When using the Consumption plan, we recommend you always put each app in its own plan, since apps are scaled independently anyway.
Consider whether you want to group functions with different load profiles. For example, if you have a function that processes many thousands of queue messages, and another that is only called occasionally but has high memory requirements, you might want to deploy them in separate function apps so they get their own sets of resources and they scale independently of each other.
Organize functions for configuration and deployment
Function apps have a
host.json file, which is used to configure advanced behavior of function triggers and the Azure Functions runtime. Changes to the
host.json file apply to all functions within the app. If you have some functions that need custom configurations, consider moving them into their own function app.
All functions in your local project are deployed together as a set of files to your function app in Azure. You might need to deploy individual functions separately or use features like deployment slots for some functions and not others. In such cases, you should deploy these functions (in separate code projects) to different function apps.
Organize functions by privilege
Connection strings and other credentials stored in application settings gives all of the functions in the function app the same set of permissions in the associated resource. Consider minimizing the number of functions with access to specific credentials by moving functions that don't use those credentials to a separate function app. You can always use techniques such as function chaining to pass data between functions in different function apps.
Scalability best practices
There are a number of factors that impact how instances of your function app scale. The details are provided in the documentation for function scaling. The following are some best practices to ensure optimal scalability of a function app.
Share and manage connections
Reuse connections to external resources whenever possible. See how to manage connections in Azure Functions.
Avoid sharing storage accounts
When you create a function app, you must associate it with a storage account. The storage account connection is maintained in the AzureWebJobsStorage application setting.
To maximize performance, use a separate storage account for each function app. This is particularly important when you have Durable Functions or Event Hub triggered functions, which both generate a high volume of storage transactions. When your application logic interacts with Azure Storage, either directly (using the Storage SDK) or through one of the storage bindings, you should use a dedicated storage account. For example, if you have an Event Hub-triggered function writing some data to blob storage, use two storage accounts—one for the function app and another for the blobs being stored by the function.
Don't mix test and production code in the same function app
Functions within a function app share resources. For example, memory is shared. If you're using a function app in production, don't add test-related functions and resources to it. It can cause unexpected overhead during production code execution.
Be careful what you load in your production function apps. Memory is averaged across each function in the app.
If you have a shared assembly referenced in multiple .NET functions, put it in a common shared folder. Otherwise, you could accidentally deploy multiple versions of the same binary that behave differently between functions.
Don't use verbose logging in production code, which has a negative performance impact.
Use async code but avoid blocking calls
Asynchronous programming is a recommended best practice, especially when blocking I/O operations are involved.
In C#, always avoid referencing the
Result property or calling
Wait method on a
Task instance. This approach can lead to thread exhaustion.
If you plan to use the HTTP or WebHook bindings, plan to avoid port exhaustion that can be caused by improper instantiation of
HttpClient. For more information, see How to manage connections in Azure Functions.
Use multiple worker processes
By default, any host instance for Functions uses a single worker process. To improve performance, especially with single-threaded runtimes like Python, use the FUNCTIONS_WORKER_PROCESS_COUNT to increase the number of worker processes per host (up to 10). Azure Functions then tries to evenly distribute simultaneous function invocations across these workers.
The FUNCTIONS_WORKER_PROCESS_COUNT applies to each host that Functions creates when scaling out your application to meet demand.
Receive messages in batch whenever possible
Some triggers like Event Hub enable receiving a batch of messages on a single invocation. Batching messages has much better performance. You can configure the max batch size in the
host.json file as detailed in the host.json reference documentation
For C# functions, you can change the type to a strongly-typed array. For example, instead of
EventData sensorEvent the method signature could be
EventData sensorEvent. For other languages, you'll need to explicitly set the cardinality property in your
many in order to enable batching as shown here.
Configure host behaviors to better handle concurrency
host.json file in the function app allows for configuration of host runtime and trigger behaviors. In addition to batching behaviors, you can manage concurrency for a number of triggers. Often adjusting the values in these options can help each instance scale appropriately for the demands of the invoked functions.
Settings in the host.json file apply across all functions within the app, within a single instance of the function. For example, if you had a function app with two HTTP functions and
maxConcurrentRequests requests set to 25, a request to either HTTP trigger would count towards the shared 25 concurrent requests. When that function app is scaled to 10 instances, the ten functions effectively allow 250 concurrent requests (10 instances * 25 concurrent requests per instance).
Other host configuration options are found in the host.json configuration article.
For more information, see the following resources: