TIL: Java Spring boot vs. Asp.Net REST API performance
Today I learned (#til) something about Java SpringBoot and Asp.Net performance.
I was testing an upcoming .Net 7 release and its Minimal APIs and I was amazed by the speed. So I decided to compare it to Java 18 SpringBoot project.
The finding was that Asp.Net 7 solution is almost 2x as fast as SpringBoot application on Java 18!
Here are the results
Asp.Net 7 Minimal APIs delivers 4,084,163 requests in 30s compared to 2,152,070 request per 30s in case of SpringBoot Java 18 application!
Java 18 SpringBoot
$ k6 run --vus 16 --duration 30s config.json
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: config.json
output: -
scenarios: (100.00%) 1 scenario, 16 max VUs, 1m0s max duration (incl. graceful stop):
* default: 16 looping VUs for 30s (gracefulStop: 30s)
running (0m30.0s), 00/16 VUs, 2152070 complete and 0 interrupted iterations
default ✓ [======================================] 16 VUs 30s
data_received..................: 373 MB 12 MB/s
data_sent......................: 392 MB 13 MB/s
http_req_blocked...............: avg=1.61µs min=441ns med=942ns max=2.39ms p(90)=1.53µs p(95)=1.75µs
http_req_connecting............: avg=308ns min=0s med=0s max=1.55ms p(90)=0s p(95)=0s
http_req_duration..............: avg=180.65µs min=114.4µs med=170.42µs max=4.11ms p(90)=202.21µs p(95)=216.99µs
{ expected_response:true }...: avg=180.65µs min=114.4µs med=170.42µs max=4.11ms p(90)=202.21µs p(95)=216.99µs
http_req_failed................: 0.00% ✓ 0 ✗ 2152070
http_req_receiving.............: avg=22.51µs min=4.59µs med=20.95µs max=3.95ms p(90)=32.44µs p(95)=39.5µs
http_req_sending...............: avg=6.3µs min=2.63µs med=5.61µs max=3.03ms p(90)=8.87µs p(95)=9.59µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=151.83µs min=92.24µs med=142.39µs max=3.43ms p(90)=172.73µs p(95)=187.11µs
http_reqs......................: 2152070 71733.676043/s
iteration_duration.............: avg=218.91µs min=138.9µs med=207.65µs max=4.54ms p(90)=244.87µs p(95)=263.15µs
iterations.....................: 2152070 71733.676043/s
vus............................: 16 min=16 max=16
vus_max........................: 16 min=16 max=16
Asp.Net 7
$ k6 run --vus 16 --duration 30s config.json
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: config.json
output: -
scenarios: (100.00%) 1 scenario, 16 max VUs, 1m0s max duration (incl. graceful stop):
* default: 16 looping VUs for 30s (gracefulStop: 30s)
running (0m30.0s), 00/16 VUs, 4084163 complete and 0 interrupted iterations
default ✓ [======================================] 16 VUs 30s
data_received..................: 845 MB 28 MB/s
data_sent......................: 743 MB 25 MB/s
http_req_blocked...............: avg=1.06µs min=431ns med=972ns max=2.48ms p(90)=1.41µs p(95)=1.62µs
http_req_connecting............: avg=0ns min=0s med=0s max=66.6µs p(90)=0s p(95)=0s
http_req_duration..............: avg=74.37µs min=30.59µs med=67.84µs max=7.33ms p(90)=91.37µs p(95)=106.97µs
{ expected_response:true }...: avg=74.37µs min=30.59µs med=67.84µs max=7.33ms p(90)=91.37µs p(95)=106.97µs
http_req_failed................: 0.00% ✓ 0 ✗ 4084163
http_req_receiving.............: avg=13.64µs min=4.43µs med=12.72µs max=7.05ms p(90)=17.75µs p(95)=19.44µs
http_req_sending...............: avg=6.36µs min=2.77µs med=5.96µs max=5.83ms p(90)=8.24µs p(95)=8.96µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=54.35µs min=19.55µs med=48.45µs max=5.34ms p(90)=70.1µs p(95)=83.96µs
http_reqs......................: 4084163 136135.973692/s
iteration_duration.............: avg=113.11µs min=56.58µs med=105.71µs max=7.9ms p(90)=133.41µs p(95)=153.52µs
iterations.....................: 4084163 136135.973692/s
vus............................: 16 min=16 max=16
vus_max........................: 16 min=16 max=16
Details
The goal was to test simple HTTP POST API that takes JSON body, deserializes it, modifies the values and returns serialized JSON response back.
The request and response objects are identical but represented by different classes. Modification of data is simple concatenation of strings and addition to integer value.
For Asp.Net project a preview7 of .Net 7 was used, for Java counterpart a new SpringBoot project with spring-boot-starter-web
and lombok
as dependencies combined with Oracle OpenJDK 18.0.2.1
The code is as follows:
Java 18 SpringBoot
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>RestApiDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>RestApiDemo</name>
<description>RestApiDemo</description>
<properties>
<java.version>18</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
RestApiDemoApplication.java
package com.example.restapidemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RestApiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RestApiDemoApplication.class, args);
}
}
InputModel.java
package com.example.restapidemo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public final class InputModel {
String FirstName;
String LastName;
Integer Age;
}
OutputModel.java
package com.example.restapidemo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@AllArgsConstructor
public final class OutputModel {
String FirstName;
String LastName;
Integer Age;
}
DemoController.java
package com.example.restapidemo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@PostMapping("/get")
@ResponseBody
public OutputModel GetData(@RequestBody InputModel input) {
return new OutputModel(input.FirstName + "x", input.LastName + "x", input.Age + 10);
}
}
Asp.Net 7
Program.cs
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/get", ([FromBody] InputModel input) => new OutputModel
{
FirstName = input.FirstName + "x",
LastName = input.LastName + "x",
Age = input.Age + 10
}
);
app.Run();
internal sealed class InputModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
internal sealed class OutputModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
K6.io config.json
import http from 'k6/http';
export default function () {
const url = 'http://localhost:8080/get';
const payload = JSON.stringify({
FirstName: 'John',
LastName: 'Doe',
Age: 33
});
const params = {
headers: {
'Content-Type': 'application/json',
},
};
http.post(url, payload, params);
}
Testing first with CURL for response validity:
Java 18 SpringBoot
$ curl -X POST http://localhost:8080/get -H "content-type: application/json" -d "{\"FirstName\":\"John\",\"LastName\":\"Doe\",\"Age\":33}"
{"firstName":"Johnx","lastName":"Doex","age":43}%
Asp.Net 7
$ curl -X POST http://localhost:8080/get -H "content-type: application/json" -d "{\"FirstName\":\"John\",\"LastName\":\"Doe\",\"Age\":33}"
{"firstName":"Johnx","lastName":"Doex","age":43}%
It seems Kestrel server with .Net 7 does a pretty decent job handling simple APIs compared to default Tomcat Server with Java 18 based SpringBoot application.
Maybe someone can consider using this when deciding to implement simple REST API since the bare language is very similar until you decide to use some ORM framework or logging.
Top comments (1)
Cool demo. In addition to Kestrel, give some credit to System.Text.Json for its fast serializer/deserializer. Any update for .NET 8?