
When it comes to hosting an API in Azure, there are a couple of options. The two main ones are Azure Function Apps and Azure Web Apps.
When I’ve investigated comparing the two online before, the results often lead to the conclusion that Function Apps are the way forward; however, I don’t think that this is always the case.
If you want to run a lightweight service that is event-driven, which importantly can include HTTP requests, then yes, a Function App might be a good choice. But what if you want to run a fully featured RESTful API? Then a Function App might not be the best choice.
I’m currently looking into this with some colleagues around what is the best approach for a new .NET API project. The project is essentially an internal search index which will, for the new iteration, likely be using a SQL database backend.
Here are its requirements:
- Be cost-effective.
- Be hosted in Azure.
- Be written in .NET.
- Serve the information over an HTTP endpoint to allow a front end to consume it.
- Be able to have IP restrictions, and it will be presented through Azure API Management.
- Have private connectivity to the SQL database.
- Be scalable.
TL;DR
- Azure Function Apps are great for lightweight, event-driven tasks and can be cost-effective on a consumption plan.
- Function Apps lack native controller support, making full REST APIs harder to build and maintain.
- Swagger and model binding support in Function Apps is limited and requires workarounds.
- Azure Web Apps support standard MVC controllers, middleware, and seamless Swagger integration.
- Web Apps offer better local development, debugging, and native authentication features.
- Web Apps provide predictable scaling and flexible networking, suitable for complex APIs.
- For fully featured, scalable APIs, Web Apps are generally the better choice; Function Apps suit smaller, event-driven workloads.
So why not just use a Function App?
Okay, so to start with, yes, a Function App can be a lot cheaper, IF you can run on a consumption plan. I don’t plan on explaining all of the different SKUs here, but essentially, with a consumption plan you just pay for the execution time you use. This is a big IF that really depends on the use case and the environment it will be used in.
During the discussion about a new project, we were having this exact debate: ‘Would a consumption plan Function App be a good choice?’. Looking at the metrics of the current system and the general traffic, it was looking good. Flurries of requests during the working hours but not hundreds of requests, and then not used out of hours. Great, so consumption plan? Well… no. We have to think about what third-party health checks may be used. In this instance, the organisation uses Better Stack which will hit the service every 3 minutes. This means the service will be running nearly all the time—so are we really going to see any benefit of the consumption plan? No, not really.
Next, we need to think about the networking side. We needed to have private networking options, including VNET integration, and the original consumption plan did not support this. Luckily, Microsoft have added the new flex consumption plan so that solves that problem.
We also need to consider the scaling options. Function Apps have event-driven scaling, so if you have a sudden spike in traffic, it can respond quickly to handle the load. Additionally, with the new flex consumption plan you can have always-ready instances to help eliminate the cold starts.
So Function Apps are starting to sound the better route, so what are the issues with what we’re wanting to do?
Firstly, Functions.
Even when set as HTTP triggers, they are not a ‘normal’ API endpoint and therefore we don’t get a standard Swagger file generated from the API. There is an old package to add this support but it is in maintenance mode and isn’t being updated. There is a version that can be used, but the latest V2.0 is only available as pre-release and therefore may not be usable depending on the environment. As Swagger uses Newtonsoft.Json as opposed to the standard function packages which is System.Text.Json. To enable Swagger, both Newtonsoft.Json and System.Text.Json would have to be added, leading to duplication of attributes on properties.
Secondly, and arguably a bigger issue, is the lack of the ControllerBase. This prevents you being able to set up controllers within your API. On top of that, the Function middleware does not handle the FromBody attribute—if used, you will simply get a null body. You obviously can develop a workaround and create your own packages to add the required support. This is what my colleagues have done to get the functionality by creating a FunctionBase, JsonBodyAttribute, and a JsonBodyExceptionMiddleware.
Lastly, without the ability to have proper controllers, you’re left with having to have ever-growing function files to have each endpoint. Where each endpoint that is required for our API becomes its own function. This is overly cumbersome and difficult to manage. We would also end up with a lot of repeated code for the set-up of each function, meaning it does not follow the DRY principle—one of the core principles of software development.
Honestly, can anyone say that this is a great idea long term? Personally, I don’t think it is. You are creating a heavy reliance on these packages that you as an organisation or team are maintaining. Increasing your maintenance overhead and likely creating technical debt, that all developers know deep down will never have the time to fix.
Well, what about a Web App instead? (my preferred option)
We can get the same networking options with VNET integration and private endpoints—as long as we use the right App Service Plan.
We also get the scaling options, metric-driven rather than event-driven. They do work as we would need them to, but it is generally slower to scale than event-driven. From personal experience it works fine—yes, not as fast, but it is still quick enough. Tip: You’ll likely want to set the rules slightly lower to scale potentially earlier than required, but it helps avoid the potential delay.
My favourite benefit is the ability to develop the API in a more standard, and arguably more maintainable way. This allows us to develop an arguably more appropriate RESTful API. We can use standard controllers to handle requests, which also enables automatic Swagger generation. We can follow the best practices, using attributes and middleware as well as being able to follow standard MVC patterns. Great—no workarounds required!
Local development is easier for developing a controller-based API. We get proper native support within IDEs such as Visual Studio or Rider. We can run the API locally with functionality like hot reload and without the need for workarounds for Function dependencies.
We also have the added flexibility to easily deploy the system to another hosting provider, be that on-premises or another cloud provider. Debugging is also easier and more intuitive as we can get more detailed stack traces and logging.
Whilst we can build middleware for hosting on a Function App, a lot of this is ready to go with a standard web API. Things such as exception handling and properly bubbling up errors for an application layer are easier to handle.
We have better ability to add authentication and authorisation to the API. For example, we can use the native [authorize]
attribute to protect endpoints. We can’t do this on a Function App—we could have used AuthorizationLevel
but this does not give us the same functionality as on a web app. In our web app we have native support for authentication such as OAuth2, OpenID Connect and JWT Bearer tokens. This includes support for multiple authentication providers, primarily for us the use of Azure Entra.
So what does the cost really look like?
Any of the costs suggested are based on the basic settings on the pricing calculator and are correct as of the day of writing. Based on a Windows-hosted application, premium functionality and currently ignoring the new flex consumption plan we have…
- Function App: EP1 SKU - £122.89
- App Service: P0V3 SKU - £88.50
Okay, so on the face of it the App Service is cheaper, right? Then you’ve got to think about the flex consumption plan. The pricing here is not yet on the calculator, and is honestly a bit harder to calculate. Firstly: “Flex consumption plan pricing includes a monthly free grant of 250,000 executions and 100,000 GB-s of resource consumption per month per subscription in pay-as-you-go on-demand pricing across all function apps in that subscription.” Azure Functions Pricing 🔗 So you’d get a reasonable amount of free hosting; you do, however, have to take into account the storage account needed for hosting the function as this is not included. You would also need to consider that if you have any always-ready instances these will use up the free quota.
How to choose?
You can probably tell my preference, but when it comes to choosing between the two options you have to consider a few things and, realistically, a choice and standard should be set across either a development team or the organisation. It is not a good idea to have a mix of the two within a team—but I’ll talk about that another day.
Nine times out of ten, if you are building a fully fledged API then going for a Web App and a standard RESTful .NET API is the way to go. It provides:
- Well-structured and maintainable code.
- Great implementation of controllers with middleware and Swagger support.
- Simpler local development workflow.
- More portable and flexible codebase.
Function Apps do have their place; they are great for lightweight, event-driven implementations. This could include:
- Timer or event-triggered tasks.
- Background processing tasks.
- Webhooks or lightweight API endpoints.
- Offloading asynchronous tasks from a main application.
So back to the debate: ‘Can we build entire APIs in a Function App?’ Yes. Should we? Realistically, no. If your team or organisation has a current standard of Function Apps for APIs then it is likely worth a discussion like we are having.
A possible suggestion of how to choose from a biased viewpoint: Your mileage may vary, and every use case can be different. I know I would always recommend a Web App for a full API, but you should always consider the specific requirements of the project. Function Apps may be lightweight and may seem ‘cheaper’, but if you are looking to build a fully featured API or trying to future-proof, then a Web App is likely the better choice.
I’d be interested to hear your thoughts, agree or disagree? Feel free to reach out on LinkedIn and let me know!