So you have a web app up and running in Azure and now you need to do some background work that’s related to your new web site but it definitely doesn’t belong in the web app itself. This could be anything from importing data, to sending out subscription emails, to updating cached data, or running database maintenance.
You may be wondering, with so many viable options at your fingertips in Azure, what is the best and most cost effective way to get work done? You want to strike the balance between having something that’s powerful enough to grow if it needs to grow but you don’t want to incur a bunch of unnecessary costs right out of the gate by choosing too much power!
The good news is that this is a common dilemma and I’ve even put together somewhat of a rubric to help guide my own decisions when I find myself in a similar situation. There are three factors I use to score my decision:
Relevance: For the work that needs to be done, how relevant is it to the web application itself. For example, sending an email based on a user interaction is very relevant. On the other hand, importing Excel data is not as relevant (but still important obviously). I like to keep relevant background work more highly coupled to the actual web app code; usually part of the same solution or project file. This doesn’t typically have much cost or performance impact, but is more of an architectural design decision.
Weight: How heavy of a lift is the work being done? Importing data could be a large task if we’re importing hundreds of thousands of row, so I consider this a heavy lift. Running database maintenance could take a long time to finish, so I also consider this heavy. Sending a single email though is something I consider a light lift and a low weight.
Frequency: How often does the work need to be done? Does it happen every time a user logs in or once a day?
In Azure, you have three main services you can choose from: Web Jobs, Functions, and Cloud Services. First, I’ll discuss how each of these services score using the above criteria. Then I’ll look at why I would choose one service over the other in several specific scenarios.
Web Jobs: Think of web jobs as a console app that runs in the same domain as your web app. Web jobs cannot exist on their own; they can only be deployed as part of an existing web application. They are very tightly coupled with your web application. This can be both a good and bad thing. It’s good because it very clearly shows (other developers) that your web job belongs together with your web app. It’s very clear and simple to understand what exactly is going on between your web job and web app. However, it can also be a bad thing because the shared deployment also means shared resources. Any CPU or RAM that your web job takes up is at the expense of your web app. This typically isn’t a big deal for web jobs that do small tasks but I’ve been in situations where this has been a problem! It can also be problematic if you wish to scale up your web job independently from your web app. Say for example, you want 5 instances of your web job running alongside your web app; the short answer is - you can’t.
So here is how web jobs stand up against the three criteria above on a scale of 1-10:
As already mentioned, web jobs are clearly tied to the web app making it a great candidate to do work that is relevant to the web app.
Web jobs are NOT a good candidate to perform heavy workloads because it shares resources with your web app and will indefinitely have a negative impact on your web app.
Oh, did I mention web jobs are “Free”? They cost nothing extra to what you are already paying for your web app. So using them to run frequent workloads is a great cost-saving idea. The only problem, as already mentioned, is that they can’t scale independently from your web app.
Functions: Azure functions are very similar to web jobs with a few key differences. It’s still helpful to think of a function as a console app but instead of being tied to a web app, it lives on its own, separate from the web app and its resources. It is similar to web jobs in how they are invoked - you must define some predefined trigger to run the function (or web job) such as a queue message, http call, event hub message, etc. Functions cannot be automatically created from your web app, they have to live in their own project and deployed separately.
The most important design decision when writing functions is whether to use the consumption plan or app service plan model. There are major cost considerations to both, which is beyond the scope of this article. Since I am strictly talking about running functions as a background processor for web app workloads, I am going to talk only about the consumption plan model.
As already mentioned, functions are not inherently tied to your web app like web jobs are but you can (should) host multiple related functions in the same project and deploy them together.
Functions are a better candidate to handle heavier workloads than web jobs because they can scale independently from the web app and they do not share the same resources as your web app. Functions can scale based on the number of pending/waiting triggers and how long the oldest trigger has been waiting to be processed. Details on how that works can be found in the Azure documentation. One thing to keep in mind is that there are some CPU and memory limitations placed on Azure functions when running on the consumption plan. Most of the time, you will not hit these limits but if you are constantly near the limit, you will likely incur higher costs than what you were planning and may be better off using Cloud Services instead.
Functions are the best for high frequency workloads. Since they can scale out automatically and you are only charged when they run, it is a great idea to rely on them to perform small to medium size workloads at frequent intervals.
Cloud Services: It is helpful to think of cloud services as a managed VM that runs separately from your web app. Cloud services are generic and can run any kind of app that you need. With this flexibility though comes a layer of complexity. Since cloud services are ultimately a VM running in the cloud, they also scale up slower than functions, take longer to deploy and, in most scenarios, will cost you the most too.
One reason I don’t like using cloud services to run background tasks for web apps is that they are too independent from the web app. Adding a cloud service to your solution requires you to add at least two new project files to start with. Also, you have to implement all of your own trigger and output bindings, unlike web jobs and functions which can make it feel very disconnected from the work that needs to be done for your web app.
Cloud services come in any size VM you want. So you can spin up a cloud service with 56 GB RAM and 8 cores if you want to!
If you need to perform light, frequent workloads, cloud services is usually not your best option. Sure, they can be set to scale out automatically, which is awesome, but every time you scale out an instance, you start getting charged immediately for that instance. Also, if there is no work to be done, you still get charged for one instance. And as mentioned already, it can take a minute or two to spin up an instance, so spinning up or down instances on the fly is not always as responsive as you need it to be.
Scenario 1: You need to import an Excel file that is sent to you every few days which contains credentials for users that need access to the web app you are working on.
In this scenario I would choose to implement a web job to complete this work. The work that is being done is light, very relevant to the web app and doesn’t need to scale up because it’s only being done once in a while.
Scenario 2: You need to update some usage statistics every time a new user logs in to your web app.
In this scenario I would either choose web jobs or function. I would probably lean towards web jobs because, again, the work being done is so closely coupled to the web app itself. However, if the statistic generation takes longer than a few seconds, I might switch to using a function so that it doesn’t negatively impact my web app performance.
Scenario 3: You need to process report subscriptions for web users who have subscribed to reports via email. The report is sent using an attached PDF file.
In this scenario I would not choose web jobs. This would do much better as a function or possibly even a cloud service, depending on how long it takes to generate the reports. Generating a PDF and sending an email is just too much work for a web job if it has to do this for possibly hundreds or thousands of users. Even if this task only runs once a day, the performance of the web app would suffer during report generation time. A function that runs on a timer would be a good candidate to do this work, but keep in mind, that even functions have a max run time of 10 minutes. If the report generation process can’t be architected in a way so that each run takes less than 10 minutes then you would most likely need to move this task to a full cloud service.
Scenario 4: You need to import sensor data in near realtime from several IoT devices and display the results immediately in your web app.
Using a web job is out of the question for this task. You will need to use functions or cloud services because it sounds like this process needs to be able to scale up and down based on the amount of data and number of devices that need to be processed. Functions should be able to handle a scenario like this just fine but if there are other auxiliary tasks, like updating cache objects, kicking off other cloud processes, etc that could take up a lot of processing time then it might be best to choose cloud services for cost reasons.
Well, I hope this information has been helpful for the next time a decision between web jobs, functions, and cloud services has to be made. Most of the decision boils down to scalability vs cost. Web jobs are free but not scalable. Functions are scalable but not free - although they can be very inexpensive as long as the work being done doesn’t take up much CPU or Memory (this is how Functions are billed on the consumption plan). And cloud services are scalable but typically the most expensive - however, sometimes there is no choice but to use a cloud service due to the complexity of the work being done.
If you need any help in understanding how to best keep your Azure costs down by choosing the right services to process background work, please reach out to me.