Large HTTP request bodies are common in modern APIs. Applications upload JSON documents, bulk records, telemetry batches, configuration data, and other structured payloads that can quickly become expensive to transmit over the network.
Compression can reduce the number of bytes sent between clients and servers, but it is not free.
The server and client must spend CPU time compressing and decompressing data. Compression can also affect latency, memory usage, throughput, and infrastructure costs.
That creates a practical engineering question:
When does HTTP request compression provide enough network savings to justify its CPU and latency overhead?
There is no universal answer. The result depends on payload size, payload structure, compression algorithm, compression level, network conditions, CPU availability, and request concurrency.
This article builds a practical benchmarking approach for large request payloads in ASP.NET Core and focuses on measuring the actual trade-offs rather than assuming that compression is always beneficial.
Why Request Compression Matters
Consider an API receiving a 10 MB JSON payload.
Without compression:
Client
|
| 10 MB
v
Network
|
v
ASP.NET Core
With compression:
Client
|
| 1.5 MB compressed
v
Network
|
v
ASP.NET Core
|
| Decompress
v
10 MB application payload
The network transfers significantly fewer bytes, but the server now has additional work to perform.
The overall request cost can be thought of as:
Total Request Cost
=
Compression Cost
+
Network Transfer Cost
+
Decompression Cost
+
Application Processing Cost
Compression is useful when reducing network transfer produces a meaningful benefit relative to the additional CPU and latency cost.
For large payloads, that trade-off can be very different from small request bodies.
Request Compression vs Response Compression
These concepts are easy to confuse.
Response Compression
The server compresses a response before sending it to the client.
Server
|
| Compress
v
Network
|
v
Client
ASP.NET Core provides response compression middleware for this scenario.
Request Compression
The client compresses the request body before sending it.
Client
|
| Compress
v
Network
|
v
Server
|
| Decompress
v
Application
This article focuses on the second scenario.
Request compression can be especially useful for APIs that receive large structured payloads over bandwidth-constrained or high-latency networks.
How HTTP Request Compression Works
HTTP uses the Content-Encoding header to indicate that a request body has been encoded.
For example:
POST /api/orders/import HTTP/1.1
Content-Type: application/json
Content-Encoding: gzip
Content-Length: 1523487
The server can determine how to decode the body from the Content-Encoding value.
The important distinction is:
Content-Type
describes the underlying media type.
Content-Encoding
describes how that representation has been encoded for transport.
A compressed JSON request can therefore have:
Content-Type: application/json
Content-Encoding: gzip
The server should decompress the request before application-level JSON processing.
Choose Payloads That Represent Real Workloads
A benchmark is only useful when the payload resembles the data the application actually receives.
For example, a synthetic payload containing repeated characters can compress extremely well:
{
"value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
That is not representative of many real applications.
A more realistic payload might contain:
{
"customerId": 12345,
"createdAt": "2026-08-17T10:00:00Z",
"orders": [
{
"id": 1001,
"product": "Product A",
"quantity": 2,
"price": 149.99
}
]
}
For a meaningful benchmark, include payloads with different characteristics.
| Payload | Approximate Size | Data Characteristics |
|---|---|---|
| Small JSON | 10–50 KB | Typical API request |
| Medium JSON | 500 KB–2 MB | Batch request |
| Large JSON | 5–10 MB | Bulk operation |
| Very large JSON | 25+ MB | Import/data processing |
The actual sizes should reflect the application's workload.
Build a Simple ASP.NET Core Endpoint
A benchmark endpoint can accept a large request body.
For example:
app.MapPost("/api/import", async (
HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var json = await reader.ReadToEndAsync();
return Results.Ok(new
{
ReceivedBytes = json.Length
});
});
This is deliberately simple.
The goal is to isolate transport and request-processing behavior rather than benchmark business logic.
For a production-oriented test, replace the artificial processing with representative application work.
Generate a Large Request
A simple .NET client can create a payload and send it to the API.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var orders = Enumerable.Range(1, 100_000)
.Select(i => new
{
Id = i,
CustomerId = i % 5000,
Product = $"Product-{i % 100}",
Quantity = (i % 5) + 1,
Amount = (i % 1000) + 10
})
.ToArray();
var json = JsonSerializer.Serialize(orders);
Console.WriteLine($"Original size: {Encoding.UTF8.GetByteCount(json)} bytes");
This gives the benchmark a reasonably structured JSON workload.
Do not assume that the resulting compression ratio will represent every production payload. Different data structures produce different results.
Compress the Request
For gzip, the request can be compressed before transmission.
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Text;
var jsonBytes = Encoding.UTF8.GetBytes(json);
await using var output = new MemoryStream();
await using (var gzip = new GZipStream(
output,
CompressionLevel.Fastest,
leaveOpen: true))
{
await gzip.WriteAsync(jsonBytes);
}
var compressedBytes = output.ToArray();
using var content = new ByteArrayContent(compressedBytes);
content.Headers.ContentType =
new MediaTypeHeaderValue("application/json");
content.Headers.ContentEncoding.Add("gzip");
using var response = await client.PostAsync(
"https://localhost:5001/api/import",
content);
The important pieces are:
content.Headers.ContentType =
new MediaTypeHeaderValue("application/json");
content.Headers.ContentEncoding.Add("gzip");
The first describes the original content.
The second tells the server how the request body has been encoded.
Measure Compression Ratio
Before measuring request latency, calculate how much the payload actually shrinks.
double ratio =
(double)compressedBytes.Length / jsonBytes.Length;
double savings =
1.0 - ratio;
Console.WriteLine(
$"Compression ratio: {ratio:P2}");
Console.WriteLine(
$"Network savings: {savings:P2}");
For example, if a 10 MB payload becomes 2 MB:
Original: 10 MB
Compressed: 2 MB
Savings: 80%
That is a significant network reduction.
However, it does not automatically mean the compressed request is faster.
Benchmark CPU and Latency Together
A useful benchmark should capture at least:
Original payload size
Compressed payload size
Compression ratio
Compression duration
Decompression duration
Request latency
Server CPU
Client CPU
Error rate
Requests per second
A simple client-side timing measurement is:
var stopwatch = Stopwatch.StartNew();
using var response = await client.PostAsync(
"https://localhost:5001/api/import",
content);
stopwatch.Stop();
Console.WriteLine(
$"Request time: {stopwatch.ElapsedMilliseconds} ms");
This gives you end-to-end request time.
For production-oriented benchmarking, also measure compression and decompression separately.
Otherwise, you cannot determine whether additional latency comes from compression, networking, or application processing.
Compare Compression Levels
Gzip supports different compression levels.
A useful benchmark can compare:
CompressionLevel.Fastest
CompressionLevel.Optimal
The goal is not to find the theoretically smallest payload.
The goal is to determine which setting produces the best result for the application's workload.
For example:
| Compression | Payload Size | CPU Time | Request Latency |
|---|---|---|---|
| None | Measure | Lowest | Measure |
| Fastest | Measure | Measure | Measure |
| Optimal | Measure | Measure | Measure |
Join the conversation! Your thoughts help the community grow.