In a project I'm part of, I found that we had some code that gzipped some requests before sending them to an external API. I found it because of some performance problems, especially with larger files. More specifically, we ran out of memory...
Here's the code:
internal async ValueTask<HttpRequestMessage> CreateRequest(Uri uri, Stream source)
{
var outStream = new MemoryStream();
await using (var zipStream = new GZipStream(outStream, CompressionMode.Compress, leaveOpen: true))
{
await source.CopyToAsync(zipStream);
zipStream.Close();
}
outStream.Position = 0;
var request = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new StreamContent(outStream)
};
request.Content.Headers.ContentEncoding.Add("gzip");
return request;
}
It was then used like this:
var uri = new Uri("https://dummy.localhost/some-endpoint");
var someLargeFile = File.Open("some-really-large-file.txt");
var request = await CreateRequest(uri, someLargeFile);
Usually, this code just worked. However, sometimes we needed to send a lot of these requests. And every time a request was sent, the gzipped content would be created in memory (thanks to the MemoryStream) before it would be sent. And since memory is a finite resource, the app would go boom.
My idea was that, surely, it should be possible to do the gzip compression on the fly instead?
I came up with the following:
public class GzipContent : HttpContent
{
private readonly Stream _sourceStream;
public GzipContent(Stream sourceStream)
{
_sourceStream = sourceStream;
Headers.ContentEncoding.Add("gzip");
}
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
await using var gzipStream = new GZipStream(stream, CompressionMode.Compress, leaveOpen: true);
await _sourceStream.CopyToAsync(gzipStream);
}
protected override bool TryComputeLength(out long length)
{
length = -1;
return false;
}
}
You use it in the same way like this:
var uri = new Uri("https://dummy.localhost/some-endpoint");
var someLargeFile = File.Open("some-really-large-file.txt");
var request = await CreateRequest(uri, someLargeFile);
// ValueTask<> is used since the `CreateRequest` method is part of an interface and to make the benchmark more fair as well.
internal ValueTask<HttpRequestMessage> CreateRequest(Uri uri, Stream source)
{
return ValueTask.FromResult(new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new GzipContent(source)
});
}
Performance
I benchmarked this with a couple of different json files of different sizes. The GzipContent version runs a tad slower for smaller files, but it allocates way less, thus solving our memory problem. The bigger the file, the bigger the difference.
BenchmarkDotNet v0.15.2, macOS 26.0 (25A353) [Darwin 25.0.0]
Apple M4 Max, 1 CPU, 16 logical and 16 physical cores
.NET SDK 10.0.100-preview.7.25380.108
[Host] : .NET 9.0.7 (9.0.725.31616), Arm64 RyuJIT AdvSIMD
.NET 9.0 : .NET 9.0.7 (9.0.725.31616), Arm64 RyuJIT AdvSIMD
Job=.NET 9.0 Runtime=.NET 9.0 InvocationCount=1
UnrollFactor=1
| Method | Filename | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio |
|------------- |----------- |------------:|----------:|----------:|------------:|------:|--------:|-----------:|------------:|
| MemoryStream | 64KB.json | 238.5 us | 7.20 us | 19.96 us | 234.7 us | 1.01 | 0.11 | 27.69 KB | 1.00 |
| GzipContent | 64KB.json | 244.1 us | 7.87 us | 21.81 us | 239.1 us | 1.03 | 0.12 | 7.94 KB | 0.29 |
| | | | | | | | | | |
| MemoryStream | 128KB.json | 763.4 us | 25.93 us | 73.54 us | 737.8 us | 1.01 | 0.13 | 123.85 KB | 1.00 |
| GzipContent | 128KB.json | 735.2 us | 14.62 us | 36.13 us | 730.6 us | 0.97 | 0.10 | 7.66 KB | 0.06 |
| | | | | | | | | | |
| MemoryStream | 256KB.json | 702.9 us | 13.99 us | 30.11 us | 701.1 us | 1.00 | 0.06 | 123.85 KB | 1.00 |
| GzipContent | 256KB.json | 732.2 us | 13.12 us | 34.78 us | 731.1 us | 1.04 | 0.07 | 7.81 KB | 0.06 |
| | | | | | | | | | |
| MemoryStream | 512KB.json | 1,360.0 us | 27.13 us | 52.91 us | 1,354.7 us | 1.00 | 0.05 | 123.85 KB | 1.00 |
| GzipContent | 512KB.json | 1,425.9 us | 28.31 us | 62.74 us | 1,431.2 us | 1.05 | 0.06 | 8.3 KB | 0.07 |
| | | | | | | | | | |
| MemoryStream | 1MB.json | 2,663.8 us | 52.87 us | 144.73 us | 2,647.0 us | 1.00 | 0.08 | 252.03 KB | 1.00 |
| GzipContent | 1MB.json | 2,744.1 us | 54.47 us | 130.50 us | 2,732.7 us | 1.03 | 0.07 | 9.05 KB | 0.04 |
| | | | | | | | | | |
| MemoryStream | 5MB.json | 12,914.2 us | 249.61 us | 324.56 us | 12,870.5 us | 1.00 | 0.03 | 2046.29 KB | 1.000 |
| GzipContent | 5MB.json | 12,802.1 us | 250.62 us | 234.43 us | 12,777.7 us | 0.99 | 0.03 | 16.28 KB | 0.008 |