In today's digital age, speed is paramount for web and mobile applications. Users expect instantaneous responses, and any delay can lead to a poor user experience and loss of engagement. Therefore, as developers, it's crucial to build web APIs that are not only functional but also optimized for performance.
ASP.NET Core offers multiple options for creating APIs, with minimal APIs and controller-based APIs being two of the most favored choices.
In this blog, we'll explore the performance of ASP.NET Core Web API by creating a simple response API using ASP.NET Core 8.0. The goal of this article is not to test real-world performance scenarios but to see how quickly we can process the simplest request through the ASP.NET Core pipeline using k6 tests, both locally and on Azure App Service with B2 and P2V3 plans.
Setting up the test environment
Before we dive into testing, let's set up the test environment. We will create two types of APIs using ASP.NET Core:
Minimal API: A lightweight approach introduced in .NET 6 that allows for a more streamlined and concise way to define endpoints.
Controller-based API: The traditional approach, providing a structured way to build APIs.
Create the ASP.NET Core Project
Create the project using either the dotnet new webapi -o "Web API Performance"
command or the Visual Studio GUI.
The .csproj
would be:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Web_API_Performance</RootNamespace>
</PropertyGroup>
</Project>
Define the minimal API in the Program.cs
file:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.MapGet("/api/hello", () => "Hello, World!");
app.MapGet("/api/weather", () =>
{
return new { Temperature = "20°C", Condition = "Sunny" };
});
app.Run();
Define the controller in the Controllers
folder:
// SimpleController.cs
using Microsoft.AspNetCore.Mvc;
namespace Web_API_Performance.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class SimpleController : ControllerBase
{
[HttpGet("hello")]
public string Get()
{
return "Hello, World!";
}
[HttpGet("weather")]
public IActionResult GetWeather()
{
var weather = new
{
Temperature = "20°C",
Condition = "Sunny"
};
return Ok(weather);
}
}
}
This setup will yield four APIs:
Minimal APIs:
/api/hello
and/api/weather
Controller APIs:
/api/simple/hello
and/api/simple/weather
Deploying to Azure App Service
Once the APIs are set up and running locally, the next step is to deploy them to Azure App Service. This will allow us to test the performance of our APIs on different Azure App Service Plan SKUs.
We'll create a web app with the runtime stack .NET 8 (LTS) in Azure and deploy our web API to the Central India region.
The deployed web app now contains the 4 APIs ready & running.
Azure App Service Plan SKUs for Testing
We will test the performance of our APIs on the following Azure App Service Plan SKUs:
Basic B2 - 2 core vCPU | 3.5 GB RAM
Premium v3 P2V3 - 4 core vCPU | 16 GB RAM
Unveiling Performance with k6
To effectively measure the performance of our basic APIs, we will use k6, a modern load testing tool, running tests locally and on the deployed Azure App Service. The results will be exported to a Grafana cloud instance for visual representation.
The k6 test script would be:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 1,
duration: '2m'
};
export default function() {
const res1 = http.get('https://<url>/api/hello', {
tags: { name: '<Local/Deployed> - Minimal hello' },
});
const res2 = http.get('https://<url>/api/weather', {
tags: { name: '<Local/Deployed> - Minimal weather' },
});
const res3 = http.get('https://<url>/api/simple/hello', {
tags: { name: '<Local/Deployed> - Controller hello' },
});
const res4 = http.get('https://<url>/api/simple/hello', {
tags: { name: '<Local/Deployed> - Controller weather' },
});
check(res1, {
'status is 200 of <Local/Deployed> minimal hello': (r) => r.status === 200,
})
check(res2, {
'status is 200 of <Local/Deployed> minimal weather': (r) => r.status === 200,
})
check(res3, {
'status is 200 of <Local/Deployed> controller hello': (r) => r.status === 200,
})
check(res4, {
'status is 200 of <Local/Deployed> controller weather': (r) => r.status === 200,
})
}
Running the Test Locally:
Running the ASP.NET Web API application in Release configuration on a local machine with an 11th Gen Intel(R) Core(TM) i7 processor & 32GB RAM, and testing it using k6 with 1 VUS for 2 mins yielded the following result:
The P95 for both minimal and controller APIs are ~0.69ms; that's sub-millisecond execution locally. Total requests made were 371K in 2-minute test duration with Peak RPS being ~4K req/s.
Running the Test on Basic B2 plan:
Running the Web API on the Azure App Service Basic B2 plan and testing it using k6 with 1 VU for 2 minutes yielded the following results:
The P95 for both minimal and controller APIs is ~26ms when deployed to Azure App Service. A total of 5.5K requests were made during the 2-minute test duration, with a peak RPS of 47.3 req/s.
Azure Speed Test shows an average latency of 23ms to 26ms from the local machine to the deployed Azure Central India data center which justifies the time taken by the API. In summary, the deployed instance is also performing efficiently, with response time close to or under one millisecond, excluding network latency.
Running the Test on Premium v3 P2v3 plan:
Running the Web API on the Azure App Service Premium P2v3 plan and testing it using k6 with 1 VU for 2 minutes yielded the following result:
The P95 for both minimal and controller APIs are ~26ms when deployed to Azure App Service P2V3 plan. Total requests made were 5.7K in the 2-minute test duration with Peak RPS being 50 req/s. The results are nearly indistinguishable, with minor discrepancies due to network latency between the test machine and the datacenter.
Analyzing the Results
After running the tests and gathering the performance metrics, several points stand out:
Minimal vs. Controller APIs: In .NET 8, there's negligible execution time disparity between the two.
Local Performance: Sub-millisecond response time underscores ASP.NET Core's efficiency.
Azure Realms: Network latency emerges as the primary factor, resulting in comparable performance across App Service Plan SKUs.
Summary
Our tests on basic ASP.NET Web APIs present a clear picture: both minimal and controller-based APIs demonstrate impressive performance with sub-millisecond response time in a local environment. However, when deployed to Azure, performance differences emerge due to network latency and the chosen App Service Plan's capabilities. The Azure App Service Premium tier offers marginally better performance than the Basic tier, but network latency remains a critical factor.
It's important to note that this test was conducted to measure basic performance characteristics with very simple APIs, not real-world scenarios. As a result, there isn't much difference in performance between the Basic B2 and Premium P2V3 tiers. However, in real-world applications, where higher processing power is required, the Premium tier will definitely outperform.
These insights highlight not only the importance of considering both infrastructure and network conditions when optimizing API performance but also demonstrate how efficiently ASP.NET Core Web API can handle requests.