A Tale of Two Serverless Platforms

October 08, 2019

Ever since I embraced serverless as The Future of Back-end Development™, I have exclusively used Azure Functions as my platform of choice. In recent months, however, I’ve seen a lot of cool news coming out of ZEIT about the capabilities of their serverless deployment platform, Now. As Now has become my go-to for static site deployments, I decided I would try Now’s Lambda support and see if it would finally meet my needs for a unified front-end and back-end solution.1 I thought I’d write up a comparison of the pros and cons of using Azure Functions and ZEIT Now, in the hope it might be useful to others looking to choose a serverless platform for a project.

Languages

Azure Functions supports a fairly broad variety of languages. JavaScript on Node 8 or 10 is supported in general availability, as well as Java and Python 3. PowerShell is supported as a preview, and TypeScript is supported via JavaScript transpilation.

Where Azure Functions shines is its first-class .NET Core support. You can write Function apps in C# or F#, with access to NuGet and its full ecosystem of .NET Core libraries, including popular tools like Entity Framework.

Where Azure Functions falls short is TypeScript. Despite being a Microsoft-developed language, the support for TypeScript in Azure Functions is second-class. You have to set up transpilation yourself and do it locally before deployment, or as part of a custom build step, because the Azure-based runtime and default build process can’t automatically handle TypeScript.

ZEIT Now has a more limited set of languages to work with. You can use JavaScript, also on Node 8 or 10, and by extension TypeScript. Beyond that, you can use the popular Go language, or Python 3.

In my opinion, Now’s standout language support feature is TypeScript. Unlike Azure Functions, TypeScript is handled automatically. Use a .ts file instead of .js for any given Lambda, and it just works. You can add a tsconfig.json to the root of your project for additional configuration if you want/need it.

Compared to Azure Functions, Now’s language support is definitely less extensive, with no option for .NET or Java, or other popular server-side languages like PHP or Ruby.

Boilerplate

One of the big selling points for serverless is the impressive reduction in boilerplate code, that stuff you have to do for every project even though you don’t really understand it. (If you haven’t tried serverless, you’re missing out for this reason alone.) But the exact amount of boilerplate that is still required does vary between platforms.

Azure Functions, like serverless in general, has pretty minimal boilerplate. You have a few configuration files that apply to the whole project, and then a configuration file for each Function. Configuration is generally in JSON, which is easy to work with and follows a well-defined schema. As far as your code itself, writing Functions in .NET does seem to require more boilerplate than JavaScript, and I’ve found the process of adding trigger extensions (see more below) to be a little obtuse and easy to mess up.

ZEIT Now has been heavily touting their new “zero-config deployments” feature, which does work with Lambdas, and honestly is just as remarkable as advertised. You can literally run npm init -y to create a package.json, and then create a file at api/example.js with the following contents:

export default (req, res) => {
	res.json({ success: true });
};

And BOOM, your project is deployable. There basically isn’t any boilerplate required for basic functionality, and you only have to add a little if you need more configurability.

Development

Local development for Azure Functions happens via the Azure Functions Core Tools and the .NET Core CLI. In general, this toolkit is pretty developer-friendly. As with boilerplate, I do find it less intuitive with .NET than with JavaScript, due to the complexities introduced by needing to compile .NET code. When developing Functions locally, you can trigger them via HTTP even if you don’t otherwise have that trigger set up, so that you can test your code easily. You can create a local.settings.json file, which you do not check in, to provide local environment variables if needed. Visual Studio Code provides some nice GUI options (via an official extension) for managing Functions projects. Visual Studio also supports Functions projects, if heavyweight IDEs are your thing.

ZEIT Now has the Now CLI, which features the now dev command for local development. This command launches a mini Now deployment on your own machine, including any required builds, to simulate the cloud environment as closely as possible. It’s extremely simple, and it seems to much more accurately mimic its server-side runtime than Azure Functions does. The killer feature here is that if you are pairing your API with a front-end that you are also deploying through Now, now dev can run it all at once for you, rather than having to run separate processes for your front-end and back-end code.

Triggers

One major feature of Azure Functions is its ability to support “triggers,” or various ways of invoking a Function other than HTTP(S) calls, and “bindings” to various non-HTTP(S) inputs and outputs. Functions can be triggered by happenings in Azure Storage accounts, Cosmos DB, Event Grid, and Service Bus, in addition to good, old-fashioned timers, as well as other Azure and .NET services. Many of these can also be hooked up as output bindings, allowing Functions to handle service-to-service interaction without any need for webhooks, cron jobs, or other more traditional mechanisms. As far as I know, ZEIT Now does not have an analogous capability.

Deployment

The main drawback to serverless at this point is its proprietary nature. Because there does have to be some underlying framework or engine to deliver the low boilerplate promises of serverless architecture, you can’t usually write a single function/lambda and deploy that exact code to Azure Functions, ZEIT Now, AWS Lambda, and Google Cloud Functions and expect it to just magically work on all of them.2 It’s easier than with more traditional architectures, as they have a lot more proprietary boilerplate to worry about, but it’s not “write once, run anywhere.”

As such, Azure Functions can be deployed to Azure, period. This isn’t necessarily a bad thing, as Azure offers a wide variety of high-quality cloud services for your Functions to integrate with. Most of that integration isn’t automatic though, and a lot of it will add to your monthly bill.

While you can theoretically hook up any build system you want, the traditional default has been Kudu, the same engine that deploys App Services. (Function Apps in Azure are essentially specialized App Services.) In my experience, Kudu can be a huge pain to work with, though it tends to be easier with Functions because their configuration and build steps are so standardized.3 Last time I set up a new Function App, however, I was given the option of using Azure Pipelines, the new CI/CD tool from Microsoft that is part of Azure DevOps (formerly Visual Studio Team Services). In just a few clicks, I had a pipeline set up using a default Function App configuration, which was pretty impressive to me.

Once in Azure, you get the ability to configure app settings or test Functions through the Azure Portal. If you’re content with an *.azurewebsites.net web address, you get SSL, but if you want a custom domain, you won’t get SSL unless you pay extra and jump through some hoops to set it up. Environments work much the same way they do in other App Services: you can create “slots” to deploy to, and swap their contents for zero-downtime deployment to production.

Following the “write-once, run-only-here” pattern, ZEIT Now Lambdas can be deployed with ZEIT Now to their own cloud, period. Now doesn’t have the wide variety of other popular cloud services that Azure does, but they do provide automatic CDN integration with customizable cache times that can be configured in code on a per-Lambda basis. And, as previously mentioned, you can deploy your front-end together with your Lambdas in the same deployment.

Now has made painless deployments its major selling point, and the Lambdas are no exception. You just run now in your local working directory, and that’s basically it. Custom domains require extremely minimal additional configuration, and you get SSL built-in without having to do a thing. (Anyone who’s dealt with setting up SSL certificates knows what a big deal that is.)

Every deployment with Now is a fresh environment with its own URL, so you can deploy every commit, every pull request, or every branch without any additional setup. Deployment for production works by just aliasing your production address to a particular deployment. You just specify your production domain in now.json and build-deploy-and-alias in one go with now --prod.

Pricing

I saved this section until the end because, in general, I think you get better results by asking What is the best option I can justify paying for? rather than simply What is the cheapest option?. In software development, it’s often said that “cheap” is expensive, because poorer-fit solutions tend to carry hidden costs in the form of longer delivery times and frustrated developers. But obviously, pricing does need to be addressed, especially for the sake of hobbyists, for whom cloud services for pet projects can quickly become cost-prohibitive.

Azure Functions are included in Azure’s free tier. You get one million executions per month for free, which is pretty substantial. Worth noting, however, is that every Function App requires an accompanying Azure Storage account, which is automatically created and is not included in the free grant. Execution time and memory usage play a role as well, so larger and/or slower Functions will cost more.

Free stuff aside, Azure Functions cost $0.20 per million executions. Alternatively, if you have an App Service plan, your Functions can run within that at its regular rates. As previously mentioned, using a custom domain will cost more if you need SSL (and in 2019, you need SSL).

ZEIT Now also has a free tier. You get 5,000 invocations (builds + requests) per day, which adds up to 150,000 per month (30 days), which is quite a lot less than Azure Functions. However, there are no sidecar costs like Azure Functions’s storage account. Lambda execution time is limited to 10 seconds per invocation, but neither execution time nor memory usage carry any additional cost under the free plan. You are limited to 100 GB/month of bandwidth and 1,000 lines/day of logging. There is no additional cost to using custom domains with SSL either.

As for the paid plan, the limits all go away (except for a 5-minute limit on each request), but it seems like the free grants do too. Execution time and memory usage will cost a little more than twice what Azure charges, and invocations themselves work out to $0.40 per million, also twice the cost of Azure. You’ll also start paying one cent per 1,000 lines logged, $0.10 per GB of bandwidth, and $0.99/month for each third-party domain you use (defined as domains you’re not purchasing or renewing through ZEIT). This doesn’t seem like it will add up substantially unless your traffic is really off the charts, but it does increase faster than Azure Functions once you grow past the baseline.

Conclusion

First, let’s get this out of the way: Azure Functions and ZEIT Now are both solid serverless platforms with great features. Neither is a “bad” option here. But they’re not equally good across every use-case.

Azure Functions has better language support, more convenient integration with Azure’s vast network of services, and cheaper pricing once you go beyond the free tier. ZEIT Now has easier development and deployment, and a more predictable cost structure.

My personal opinion is that very large-scale projects that expect massive monthly traffic and have a well-funded team supporting them should stick with Azure Functions. You should also consider Azure Functions if you need to work in a language other than JavaScript, TypeScript, Go, and Python, or you have critical libraries in .NET or Java that would be expensive to port. And of course, if you have a need for advanced trigger behavior, especially if you’re using Azure services, then Azure Functions is really the only logical way to go.

However, if those factors don’t apply to your project, if you are a hobbyist, you are building small-scale things that will fit within Now’s free plan (or incur very small costs under the paid plan), and/or you have a small team (or no team) with limited time to invest, then I would recommend ZEIT Now due to its much lower complexity across building, deploying, and billing. I will likely use ZEIT Now for many personal projects going forward, as it’s simply a much easier development and deployment experience.

Azure Functions is the more powerful option, but ZEIT Now is by far the fastest and easiest way to spin up a serverless API that I’ve seen so far, plus it has the added benefit of zero-config front-end in conjunction with tools like Create React App.


1 ASP.NET Core MVC provides this, but I’ve never been able to get into Razor, the heavyweight API model, or the MVC pattern.

2 There are third-party frameworks out there that do facilitate this. But a third-party abstraction layer is not the same as standardization or cross-platform compatibility.

3 This may also be because I do not live in a Windows development toolchain. Developers who have deep experience with Visual Studio, PowerShell, MSBuild, IIS, and/or other Windows dev tools may find Kudu more natural than I do.


Philip Fulgham is a software engineer who builds web applications. Visit this website's front page to learn more.