I’ve recently started using Azure Functions. What interested me most was to understand why users are picking Functions over “plain” Azure Web Apps to host HTTP APIs. I found the HTTP binding to be limited, the development experience to be clunky and the Application Insights integration to behave differently that on ASP.NET Core.
In this post I’ll describe the limitations I faced when working with Azure Functions. In follow-up posts I’ll describe the workarounds I’m using to address some of these limitations.
I created a sample project to demonstrate the behaviours I’m describing in this post.
Azure Functions do not support middleware out of the box. I don’t think I’ve built a single ASP.NET Core API in the past without using middleware. The Framework itself is relying on them: from authentication, health check to header propagation (list of built-in middleware). This makes implementing cross-cutting concerns problematic. As soon as you have more than one Function in your app, you’ll need to decide how to implement authorisation, validation…The Azure Functions team is working on an out-of-process .NET 5 worker that will introduce “a customizable middleware pipeline”.
There is no TestServer. This makes integration tests harder as you can’t run the whole pipeline in-memory. You can’t easily verify whether you’re using
PascalCase for serialising properties’ name for example. This also means that you’re left with asserting the returned object rather than an HTTP response. This can lead developers to test implementation details such as whether the Function returned a
StatusCodeResult or an
ObjectResult instead of asserting the returned status code. Finally, developers need to craft an
HttpRequest when calling a Function in a test.
When running on a consumption plan, you’ll experience cold starts. Most of the time an Azure Function will “only” take a few seconds to wake-up but sometimes it can take more than 30 seconds. If your APIs are users facing, this results in a poor user experience.
The Azure Functions tooling uses a custom console logging provider. This provider does not display the stack trace when an exception is thrown. If you want to know where an exception originated, you’ll have to run with a debugger attached.
EasyAuth is not supported when running locally. This is not a limitation of Azure Functions but of EasyAuth itself.
Running Functions over HTTPS on Windows is clunky.
Application Insights integration
Azure Functions are automatically monitored by Application Insights. All you need is an instrumentation key. For a team that is getting started on the observability journey, this is great. They’ll get insights into how fast their Functions are, they’ll get dependencies and exceptions tracking. But this integration comes with some downsides as well. With the default configuration, Azure Functions emit a lot of telemetry. This translates to Application Insights costing a hundred times as much as the Functions they’re monitoring!
Each Function execution logs two traces: one when it starts executing the Function and one when it has executed the Function:
Exceptions are logged twice for the HTTP binding:
Exceptions are logged three times for the Service Bus binding:
In fact, for this execution, the Functions runtime emitted eight telemetry items:
Telemetry Processor is not supported out of the box. It is discussed at length in this GitHub issue. This might seem like a minor issue, but it makes it harder to filter out superfluous telemetry items.
Finally trying to retrieve the
TelemetryConfiguration from the container without having set the
APPINSIGHTS_INSTRUMENTATIONKEY setting results in an exception:
This is not a limitation of Azure Functions but a common issue I see with existing Functions. They lack the
RUN_FROM_PACKAGE setting. In some rare cases this results in deployments being marked as successful even though the code was not deployed. Run from a package has a section dedicated to it in the Functions documentation.
In this post I listed the inconveniences I experienced while working with Azure Functions. In order of preference here are the issues I’d like to see fixed the most:
- Lack of telemetry processor support
- Lack of middleware support
- Lack of in-memory integration tests support
- No stack trace in the console
- Clunky local HTTPS support
In the following posts I’ll be describing some workarounds I’m using.