Rest vs gRPC - performance benchmark in .Net Core 3.1
Hey Guys!
The last days in Poland are cold & cloudy. This weather is a great motivator to reflect on… Communication protocols :-)!
This article presents…
- gRPC CodeFirst approach,
- The performance benchmark for gRPC vs REST communication in .Net core 3.1,
- How many does cost to open gRPC channel & why is worth to scope it like HttpClient,
- Note: **All tests were started on my local PC, so all network traffic was occured in localhost & self signed ssl certs. **
gRPC CodeFirst approach.
Firstly, I want to show You a little different way of creating gRPC web services than Microsoft official docs. Maybe a long time ago You heard something about gRPC, something about the requirement to have a .proto contract files for generating base, service classes. Maybe, when You look at this, You were scared (I did). What if I tell You, there is a different, unofficial way of creating gRPC services? Does this different approach have this same performance, no .proto files & shorter way of implementation based on C# models, interfaces & attributes? This is protobuf-net.Grpc library created by Marc Gravell. From now, You can forget about ContractFirst and focus on creating contracts by .net coding! (btw. it’s also possible to create .proto files from it :-))!
[ServiceContract]
public interface IUserGrpcService
{
ValueTask<UserDto> ActivateAsync(ActivateUserCommand command, CallContext context = default);
ValueTask<CreatedUserDto> CreateUserAsync(CreateUserCommand command, CallContext context = default);
ValueTask<AuditedUserDto> LoginAsync(LoginCommand command, CallContext context = default);
ValueTask<ResetPasswordDto> ResetPasswordAsync(ResetPasswordCommand command, CallContext context = default);
ValueTask<UserDto> SetNewPasswordAsync(SetNewPasswordCommand command, CallContext context = default);
}
[ProtoContract]
public class UserDto
{
[ProtoMember(1)]
public long Id { get; set; }
[ProtoMember(2, DataFormat = DataFormat.Default)]
public DateTime CreatedAt { get; set; }
[ProtoMember(3)]
public string Email { get; set; }
}
// Server
public class UsersGrpcService : IUserGrpcService
{
private readonly IMediator _mediator;
public UsersGrpcService(IMediator mediator)
{
_mediator = mediator;
}
public async ValueTask<UserDto> ActivateAsync(ActivateUserCommand command, CallContext context = default)
=> await _mediator.Send(command, context.CancellationToken);
public async ValueTask<CreatedUserDto> CreateUserAsync(CreateUserCommand command, CallContext context = default)
=> await _mediator.Send(command, context.CancellationToken);
public async ValueTask<AuditedUserDto> LoginAsync(LoginCommand command, CallContext context = default)
=> await _mediator.Send(command, context.CancellationToken);
public async ValueTask<ResetPasswordDto> ResetPasswordAsync(ResetPasswordCommand command, CallContext context = default)
=> await _mediator.Send(command, context.CancellationToken);
public async ValueTask<UserDto> SetNewPasswordAsync(SetNewPasswordCommand command, CallContext context = default)
=> await _mediator.Send(command, context.CancellationToken);
}
//Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddCodeFirstGrpc(config =>
{
config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal;
config.Interceptors.Add<ExceptionInterceptor>();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<UsersGrpcService>();
});
// ...
}
//Client - create of channel and service should be done by DI.
//Opening of channel & create service costs.
//Remember about it.
using var grpc = _grpcFactory.CreateChannel(
_settings.Users.Host,
_settings.Users.Port);
var service = grpc.CreateService<IUserGrpcService>();
//Client - cosume method.
var dto = await _service.CreateUserAsync(command);
return dto;
Benchmark - environment
BenchmarkDotNet=v0.12.1, OS=macOS Catalina 10.15.3 (19D76) [Darwin 19.3.0]
Intel Core i7-8850H CPU 2.60GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.301
[Host] : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT [AttachedDebugger]
DefaultJob : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
Benchmark - how web communication should be implemented for the best performance?
| Method | Count | Mean | Error | StdDev |
|---|---|---|---|---|
| GrpcWithChannelAndService | 1 | 11,309.2 μs | 290.53 μs | 838.25 μs |
| GrpcWithChannel | 1 | 10,860.3 μs | 294.60 μs | 854.69 μs |
| GrpcFromZero | 1 | 22,423.3 μs | 1,316.08 μs | 3,880.51 μs |
| RestFromZero | 1 | 30,700.7 μs | 1,962.80 μs | 5,101.57 μs |
| RestWithClient | 1 | 659.0 μs | 24.50 μs | 71.08 μs |
| Method | Count | Mean | Error | StdDev |
| --- | --- | --- | --- | --- |
| GrpcWithChannelAndService | 10 | 107,965.4 μs | 2,136.78 μs | 2,098.60 μs |
| GrpcWithChannel | 10 | 112,519.0 μs | 3,375.63 μs | 9,900.13 μs |
| GrpcFromZero | 10 | 215,862.6 μs | 18,452.26 μs | 54,406.91 μs |
| RestFromZero | 10 | 326,166.2 μs | 18,679.43 μs | 55,076.72 μs |
| RestWithClient | 10 | 7,089.6 μs | 378.08 μs | 1,102.88 μs |
| --- | --- | --- | --- | --- |
| GrpcWithChannelAndService | 20 | 216,042.5 μs | 6,804.51 μs | 20,063.25 μs |
| GrpcWithChannel | 20 | 203,643.7 μs | 5,668.72 μs | 16,081.22 μs |
| GrpcFromZero | 20 | 463,342.3 μs | 22,823.91 μs | 66,216.31 μs |
| RestFromZero | 20 | 652,889.9 μs | 20,515.53 μs | 58,862.91 μs |
| RestWithClient | 20 | 17,238.0 μs | 1,385.95 μs | 4,064.74 μs |
Dto Size ~ 2,5kb
As You see The clear winner is traditional method with created HttpClient. I want to focus our attention to cost of creating gRPC channel before every request, is expensive! As You see this is very simillar situation to create of instances HttpClients, is should be created with correct scope, btw. is good material for next article ;-)!
So now, when we know what is the most performant of communicate, every next benchmark will be execute with created gRPC channel & HttpClient ;-).
Benchmark - fetch dto.
| Method | Count | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| GrpcGetDtoSeriesAsync | 1 | 9,949.7 μs | 415.48 μs | 1,225.0 μs | 9,723.9 μs |
| RestGetDtoSeriesAsync | 1 | 737.1 μs | 52.39 μs | 152.8 μs | 700.3 μs |
| --- | --- | --- | --- | --- | --- |
| GrpcGetDtoSeriesAsync | 10 | 116,574.7 μs | 4,606.11 μs | 13,141.5 μs | 111,470.8 μs |
| RestGetDtoSeriesAsync | 10 | 7,570.4 μs | 540.62 μs | 1,559.8 μs | 7,274.1 μs |
| --- | --- | --- | --- | --- | --- |
| GrpcGetDtoSeriesAsync | 25 | 301,079.5 μs | 14,519.36 μs | 42,582.8 μs | 296,800.6 μs |
| RestGetDtoSeriesAsync | 25 | 36,390.8 μs | 5,441.64 μs | 16,044.8 μs | 33,363.2 μs |
| --- | --- | --- | --- | --- | --- |
| GrpcGetDtoSeriesAsync | 100 | 1,106,388.0 μs | 47,152.74 μs | 132,995.1 μs | 1,083,339.7 μs |
| RestGetDtoSeriesAsync | 100 | 162,367.5 μs | 24,483.35 μs | 71,805.4 μs | 149,657.5 μs |
Link: https://github.com/Rogaliusz/Grpc.Performance/blob/main/src/Grpc.Performance/Benchmarks/Series.cs
Dto Size ~ 2,5kb
Again, for small requests the old, good REST is clear winner :-(..
Benchmark - fetch collections.
| Method | Mean | Error | StdDev | Median |
|---|---|---|---|---|
| GrpcGetCollectionBigDtoAsync | 45.351 ms | 4.3962 ms | 12.6841 ms | 40.895 ms |
| GrpcGetPaginatedBigDtoAsync | 11.422 ms | 0.2475 ms | 0.7100 ms | 11.430 ms |
| RestGetCollectionBigDtoAsync | 78.303 ms | 1.5348 ms | 2.6062 ms | 77.985 ms |
| RestGetPaginatedBigDtoAsync | 3.356 ms | 0.0657 ms | 0.0899 ms | 3.338 ms |
Collection Size ~ 1,12 mb.
Pagination Size ~ 46 kb.
As we see for bigger payload gRPC is better solution, but probably case of send this kind large payload will be very rare :-(..
Benchmark - send items.
| Method | Count | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| GrpcSendBigCommand | 1 | 14.393 ms | 1.4331 ms | 4.112 ms | 14.775 ms |
| GrpcSendBigEventCommand | 1 | 10.697 ms | 0.3768 ms | 1.044 ms | 10.492 ms |
| RestSendBigCommand | 1 | 2.992 ms | 0.5211 ms | 1.520 ms | 2.676 ms |
| RestSendBigEventCommand | 1 | 66.549 ms | 13.4749 ms | 39.731 ms | 58.052 ms |
| Method | Count | Mean | Error | StdDev | Median |
| --- | --- | --- | --- | --- | --- |
| GrpcSendBigCommand | 10 | 94.743 ms | 2.1262 ms | 6.236 ms | 93.040 ms |
| GrpcSendBigEventCommand | 10 | 112.785 ms | 3.2183 ms | 9.337 ms | 112.019 ms |
| RestSendBigCommand | 10 | 19.546 ms | 1.1930 ms | 3.325 ms | 19.538 ms |
| RestSendBigEventCommand | 10 | 551.404 ms | 114.6069 ms | 337.921 ms | 509.745 ms |
| Method | Count | Mean | Error | StdDev | Median |
| --- | --- | --- | --- | --- | --- |
| GrpcSendBigCommand | 20 | 214.305 ms | 4.7126 ms | 13.747 ms | 212.344 ms |
| GrpcSendBigEventCommand | 20 | 211.541 ms | 5.6016 ms | 16.429 ms | 208.450 ms |
| RestSendBigCommand | 20 | 71.923 ms | 4.8336 ms | 14.252 ms | 71.736 ms |
| RestSendBigEventCommand | 20 | 1,161.713 ms | 194.5069 ms | 573.508 ms | 1,149.660 ms |
Link: https://github.com/Rogaliusz/Grpc.Performance/blob/main/src/Grpc.Performance/Benchmarks/Post.cs
BigCommand - flat model with few properties.
BigEventCommand - model with nested 20 elements collection.
So Again, larger payload = gRPC.
Summary
I’m very surprised of this result. I suspected that gRPC protocol will be clear winner in this battle, but I was wrong. For smaller requests old json format is clear winner, gRPC is good solution only for large payload.
Maybe this result is sponsored by my local network environment. In future I want to do this benchmark in Kubernetes cluster or communication between some endpoints in the web.
One thing is clear, JSON serialization in .Net core 3.1 is really, really fast. Team made great job, respect for them ;-)!
Edit
If You want to see how results are present in Azure Kubernetes Cluster, please check my new post about it :-)
https://the-worst.dev/rest-vs-grpc-performance-benchmark-in-net-core-3-1-azure/
Tags