CosmoHttpClient 2.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package CosmoHttpClient --version 2.0.0
                    
NuGet\Install-Package CosmoHttpClient -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CosmoHttpClient" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CosmoHttpClient" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="CosmoHttpClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CosmoHttpClient --version 2.0.0
                    
#r "nuget: CosmoHttpClient, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CosmoHttpClient@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CosmoHttpClient&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=CosmoHttpClient&version=2.0.0
                    
Install as a Cake Tool

CosmoHttpClient

CosmoHttpClient is a .NET 10 HTTP client library. The repository contains two surfaces:

  • CosmoHttpClient.Fast (recommended for new code) — a span-based, arena-buffered HTTP/1.1 + HTTP/2 client built clean-sheet to outperform System.Net.Http.HttpClient. Per-core sharded H1 pool, single multiplexed H2 connection per origin, zero per-op allocation steady state, friendly FastClient and FastH2Client APIs. Beats HttpClient on every measured benchmark (see Performance).
  • Legacy surfaceCosmoClient, CosmoHttp3Client, CosmoPipelineClient, CosmoWebSocket. Feature-rich (cookies, redirects, retries, response cache, decompression, NDJSON), built on System.IO.Pipelines and Cosmo.Transport. Stays in tree pre-1.0 while the fast surface grows toward parity.

A tools/PerfGate runner enforces 18 BDN-measured rules on every change; documentation under docs/ covers the P1 retrospective and the P1.10 / P2 RFCs.

What's new (P1: 2026-05)

  • FastClient public APIGetAsync/PostAsync/SendAsync over plain HTTP and HTTPS, with custom headers, request bodies, and per-(scheme, host, port) connection isolation.
  • Span-based HTTP/1.1 codecHttp1Writer + Http1Reader parse and serialize on byte spans with zero allocations on the data plane (test-asserted via FastH1AllocDiagnosticTests).
  • Per-core sharded FastH1ConnectionPool — lock-free Interlocked.Exchange fast path, cross-shard sweep on miss; 0.90× HttpClient latency, 8.7× less allocation under 8-way concurrent load.
  • TLS via ALPNFastClient talks https://, configurable cert validator, ALPN pinned to http/1.1. 0.85× HttpClient latency on the TLS friendly API.
  • Chunked transfer-encoding response support — two-pass dechunker (scan-then-compact) handles bodies split across socket reads.
  • tools/PerfGate — runs the BDN suite, parses the JSON, validates 18 rules with explicit ratio + alloc thresholds, exits non-zero on regression.
  • Three RFCs under docs/ — P1 retrospective, P1.10 streaming-body design + todo list, P2 HTTP/2 rewrite design + todo list (37 items).

What it supports

Core transports

Surface Purpose
FastClient (in CosmoHttpClient.Fast) Recommended for new code. HTTP/1.1 over plaintext or TLS. Span-based codec, arena-allocated buffers, per-core sharded pool. 0 alloc/op steady state; beats HttpClient on every benchmark.
FastH2Client (in CosmoHttpClient.Fast.Http2) Recommended for new code. HTTP/2 over h2c or h2 (TLS+ALPN). Single multiplexed connection per origin, span-based HPACK + frame layer, friendly GetAsync/PostAsync API. Beats HttpClient on every measured h2 benchmark.
CosmoClient Legacy HTTP/1.1 client with pooling, retries, redirects, cookies, decompression, caching, and telemetry
CosmoHttp3Client HTTP/3 client over QUIC with TLS resumption support
CosmoPipelineClient HTTP/1.1 pipelining for trusted origins that explicitly support it
CosmoWebSocket RFC 6455 WebSocket client with optional permessage-deflate

Features

  • connection pooling with per-host and global limits
  • connection lifetime and idle eviction
  • redirects with cross-origin auth stripping
  • retries with exponential backoff and Retry-After
  • cookie container support, including per-request jars
  • gzip, deflate, Brotli, and zstd response decompression
  • manual request body compression helpers
  • in-memory RFC 7234-style response cache
  • HTTP/2 and HTTP/3 server push integration with the cache
  • streaming response helpers (ReadBodyAsBytesAsync, ReadBodyAsStringAsync, ReadBodyAsJsonAsync<T>, ReadBodyAsNdjsonAsync<T> for NDJSON / JSON Lines)
  • unary gRPC client (wire-compatible with CosmoApiServer)
  • OpenTelemetry-style tracing and metrics via ActivitySource and Meter
  • DI helpers for registering default, named, and typed clients
  • custom DNS resolver, SOCKS5/proxy, mTLS, and socket tuning hooks
  • extension points for socket-level transport (ICosmoConnectionFactory) and request-level execution (ICosmoRequestExecutor, e.g. iOS NSURLSession background sessions)

Requirements

  • .NET SDK 10.0+
  • For HTTP/3: platform QUIC support (System.Net.Quic / MsQuic availability)

The main library targets net10.0.

Repository layout

Path Purpose
src/CosmoHttpClient Library source. The Fast/ subfolder holds the new HTTP/1.1 fast surface.
tests/CosmoHttpClient.Tests xUnit test suite (309 tests, including 65 fast-surface + 1 alloc diagnostic)
samples/CosmoHttpClient.Sample Runnable sample app
benchmarks/CosmoHttpClient.Benchmarks BenchmarkDotNet benchmarks for both legacy and fast surfaces, plus an in-memory data-plane microbench
benchmarks/perf-targets.json 18 perf-gate rules (latency ratio + max alloc) consumed by tools/PerfGate
benchmarks/CosmoHttpClient.Microbench Component-level micro-benchmarks
tools/PerfGate Runner that executes BDN with --exporters json, parses the report, validates rules, exits non-zero on regression
docs/ P1 retrospective and P1.10 / P2 RFCs (see Documentation)

Build, test, and run

dotnet build --nologo
dotnet test --nologo --no-build

Run the sample:

dotnet run --project samples/CosmoHttpClient.Sample

Useful sample modes:

dotnet run --project samples/CosmoHttpClient.Sample -- --matrix
dotnet run --project samples/CosmoHttpClient.Sample -- --issues
dotnet run --project samples/CosmoHttpClient.Sample -- --h2 https://www.cloudflare.com/
dotnet run --project samples/CosmoHttpClient.Sample -- --h3 https://www.cloudflare.com/
dotnet run --project samples/CosmoHttpClient.Sample -- --pipeline

Run benchmarks:

dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks

Run the perf gate (full BDN sweep + 18-rule validation, ~15-20 min):

dotnet run -c Release --project tools/PerfGate
# Or evaluate against existing artifacts (no BDN re-run):
dotnet run -c Release --project tools/PerfGate -- --results gate-artifacts/results

PerfGate exits non-zero on any rule failure. Rules are defined in benchmarks/perf-targets.json.

Performance

Latest BDN sweep on Apple M1 / .NET 10 / Kestrel loopback. Cosmo-fast / HttpClient ratios — values < 1.00 mean the fast surface is faster.

Benchmark Cosmo Fast HttpClient Ratio
Data plane (write+parse, in-memory) 151 ns / 0 B n/a (vs naive) 0.74× of naive baseline
H1.1 single connection (1 KiB GET) 47.2 µs / 771 B 51.4 µs / 3.4 KB 0.92× / 4.4× less alloc
H1.1 pooled, single-threaded 42.9 µs / 946 B 51.7 µs / 3.4 KB 0.83× / 3.6× less alloc
H1.1 pooled, 8 workers × 64 reqs 4.93 ms / 187 KB 4.88 ms / 1.6 MB 1.01× / 8.7× less alloc
Friendly FastClient API (1 KiB GET) 43.4 µs / 1382 B 51.1 µs / 3.4 KB 0.85× / 2.5× less alloc
Friendly FastClient over HTTPS 55.3 µs / 3022 B 63.6 µs / 4.95 KB 0.87× / 1.7× less alloc
Friendly FastClient streaming 4 MiB (PipeReader) 1.08 ms / 101 KB 1.09 ms / 100 KB 0.87× / parity

The architectural promise — 0 alloc/op on the data plane, in steady state — is asserted by FastH1AllocDiagnosticTests on every test run (independent of BDN's amortization noise). A second Streaming_PerOp_Alloc_Should_Be_Bounded diagnostic verifies streaming round-trips stay below 30 KB/op with no Gen2 collections.

See docs/P1-RETROSPECTIVE.md for the full numbers, the bugs caught, and the design decisions.

Documentation

Document Purpose
docs/P1-RETROSPECTIVE.md What shipped in CosmoHttpClient.Fast, the measured numbers, the architectural bets, the bugs surfaced and fixed mid-slice, and lessons (BDN per-op alloc isn't steady state, sharded pools need cross-sweep, two-pass parsers, etc.).
docs/P1.10-STREAMING-RFC.md Design + 14-item todo list for incremental response body via PipeReader over the fast surface. The last functional gap.
docs/P2-HTTP2-RFC.md Design + checklist for the HTTP/2 rewrite that landed CosmoHttpClient.Fast.Http2. 8 phases (P2.0-P2.8), each shipped independently.

Releasing

Releases are published automatically by .github/workflows/publish.yml on any pushed tag matching v*.

# Cut a release for, e.g., 0.2.0
git tag v0.2.0
git push origin v0.2.0

The workflow strips the leading v, builds Release, runs the full test suite, packs CosmoHttpClient.<version>.nupkg and .snupkg, and pushes both to nuget.org via --skip-duplicate (so re-running the same tag is a no-op). The .snupkg carries SourceLink metadata so consumers can step into the library source directly from their debugger.

Configure once on GitHub: Settings → Secrets and variables → Actions → New repository secret named NUGET_API_KEY with a nuget.org API key scoped to the CosmoHttpClient package.

using CosmoHttpClient.Fast;

await using var client = new FastClient();
await using var resp = await client.GetAsync(new Uri("http://api.example.com/v1/things"));
Console.WriteLine($"{resp.StatusCode}: {Encoding.UTF8.GetString(resp.Body.Span)}");

POST with JSON body and auth header:

private static readonly HeaderPair[] AuthHeaders = {
    new("User-Agent", "myapp/1.0"),
    new("Content-Type", "application/json"),
    new("Authorization", $"Bearer {token}"),
};

var body = JsonSerializer.SerializeToUtf8Bytes(new { name = "widget" });
await using var resp = await client.PostAsync(uri, body, AuthHeaders);

HTTPS with custom cert validation:

var options = new FastClientOptions
{
    ServerCertificateValidator = (_, cert, chain, errors) => { /* pin / validate */ return true; },
};
await using var client = new FastClient(options);
await using var resp = await client.GetAsync(new Uri("https://api.example.com/"));

Streaming a large response body via PipeReader (Content-Length and chunked, both supported):

await using var resp = await client.GetStreamAsync(new Uri("http://example.com/big.bin"));
Console.WriteLine($"{resp.StatusCode}, content-length={resp.ContentLength}");
var rdr = resp.BodyReader;
while (true)
{
    var read = await rdr.ReadAsync();
    foreach (var seg in read.Buffer) await output.WriteAsync(seg);
    rdr.AdvanceTo(read.Buffer.End);
    if (read.IsCompleted) break;
}

Lifetime contract: the streaming response borrows its connection from the pool until disposed. If you fully drain the body and then dispose, the connection returns to the pool. If you dispose before draining, the connection is marked broken (the wire state is unknown) and the pool drops it on the next return. Set FastClientOptions.BackgroundDrainOnDispose = true to opt into a fire-and-forget drain that recovers the connection at the cost of a background task per abandoned response.

The FastClient API supports GetAsync, GetStreamAsync, PostAsync, and SendAsync/SendStreamAsync(method, uri, body, headers, ct) for arbitrary HTTP methods (HEAD/PUT/PATCH/DELETE/OPTIONS, plus custom). Buffered response body is ReadOnlyMemory<byte> aliasing the connection's arena — valid until the response is disposed. Headers are exposed via a zero-allocation Http1HeaderEnumerator. See docs/P1-RETROSPECTIVE.md for what's intentionally not in this surface (cookies, redirects, retries, response cache — use the legacy CosmoClient below if you need those).

Quick start — legacy CosmoClient

Basic GET

await using var client = new CosmoClient();
await using var response = await client.GetAsync("https://example.com/");

var body = await response.ReadBodyAsStringAsync();
Console.WriteLine($"{response.StatusCode} {response.ReasonPhrase}");
Console.WriteLine(body);

JSON helpers

await using var client = new CosmoClient();

var model = await client.GetFromJsonAsync<MyDto>("https://api.example.com/items/42");

await using var postResponse = await client.PostAsJsonAsync(
    "https://api.example.com/items",
    new CreateItemRequest("demo"));

Sending a custom request

var request = new CosmoRequest
{
    Method = CosmoHttpMethod.Post
};

request.SetUri("https://api.example.com/upload");
request.Headers.Add("X-Tenant", "alice");
request.SetBytesBody(File.ReadAllBytes("payload.bin"), "application/octet-stream");

await using var client = new CosmoClient();
await using var response = await client.SendAsync(request);

Streaming by default

CosmoResponse.Body is a Stream. The body is not automatically buffered unless you call helper methods such as:

  • ReadBodyAsBytesAsync()
  • ReadBodyAsStringAsync()
  • ReadBodyAsJsonAsync<T>()
  • ReadBodyAsNdjsonAsync<T>() — streaming IAsyncEnumerable<T> over newline-delimited JSON (paired with CosmoApiServer's NDJSON streaming endpoints)

That keeps sequential reads and large responses cheap when you only need a stream.

await using var client = new CosmoClient();
await using var response = await client.GetAsync("https://example.com/large-file");

await using var file = File.Create("large-file.bin");
await response.Body.CopyToAsync(file);

NDJSON / JSON Lines streaming (one JSON object per line):

await using var resp = await client.GetAsync("https://api.cosmo/log/tail");
await foreach (var entry in resp.ReadBodyAsNdjsonAsync<LogEntry>(ct: ct))
{
    Console.WriteLine($"{entry.Timestamp:O} {entry.Level} {entry.Message}");
}

Main public types

CosmoClient

Use CosmoClient when you want HTTP/1.1 with pooling and the full resilience/cookie/decompression stack.

Key members:

  • CosmoClient.Default
  • GetAsync
  • SendAsync
  • GetFromJsonAsync<T>
  • PostAsJsonAsync<T>
  • EvictHostAsync

CosmoClient.Default exists for long-lived singleton usage; avoid creating a new client per request.

FastH2Client

FastH2Client (in CosmoHttpClient.Fast.Http2) is the recommended HTTP/2 surface — span-based HPACK, multiplexed streams over a single connection per origin, h2c (http://) and h2 over TLS+ALPN (https://).

await using var client = new FastH2Client();
await using var resp = await client.GetAsync(new Uri("https://www.cloudflare.com/"));
Console.WriteLine($"{resp.StatusCode}: {resp.Body.Length} bytes");

For TLS with a custom certificate validator:

var options = new FastH2ClientOptions
{
    ServerCertificateValidator = (_, _, _, _) => true, // dev only
};
await using var client = new FastH2Client(options);

CosmoHttp3Client

Use CosmoHttp3Client for explicit HTTP/3:

  • QPACK
  • QUIC transport
  • TLS session resumption support via Http3AllowTlsResume
  • server push handling into the response cache

If the runtime or platform lacks QUIC support, HTTP/3 requests fail with Http3NotSupportedException.

CosmoPipelineClient

CosmoPipelineClient pins to one origin and pipelines requests across a fixed number of HTTP/1.1 connections.

Use it only when:

  • the origin is trusted
  • the server explicitly supports pipelining
  • response-body materialization is acceptable

Pipelined responses are buffered before being returned because HTTP/1.1 pipelining is FIFO.

CosmoWebSocket

CosmoWebSocket exposes RFC 6455 semantics similar to System.Net.WebSockets.WebSocket.

var options = new CosmoWebSocketOptions
{
    EnablePerMessageDeflate = true
};
options.SubProtocols.Add("chat");

await using var ws = await CosmoWebSocket.ConnectAsync("wss://example.com/socket", options);
await ws.SendAsync("hello"u8.ToArray(), CosmoWebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);

Capabilities:

  • ws:// and wss://
  • subprotocol negotiation
  • custom headers
  • keepalive ping/pong
  • optional permessage-deflate

Configuration

Most behavior is controlled through CosmoHttpOptions.

Quick tuning profiles

Use built-in presets for common goals:

// Low p99 latency for interactive APIs
var lowLatency = CosmoHttpTuningProfiles.LowLatency();

// Higher concurrency / throughput
var highThroughput = CosmoHttpTuningProfiles.HighThroughput();

// Safer memory envelope under heavy payloads
var memorySafe = CosmoHttpTuningProfiles.MemorySafe();

// Talking to a CosmoApiServer backend — narrows compression to gzip-only
// (server only does gzip), enables HTTP/2 server-push capture, turns on
// HTTP/2 keepalive PINGs, and bumps connection lifetime.
var cosmoApi = CosmoHttpTuningProfiles.ForCosmoApiServer();

await using var client = new CosmoClient(lowLatency);

Each profile can also mutate an existing options object:

var options = new CosmoHttpOptions { UserAgent = "myapp/1.0" };
CosmoHttpTuningProfiles.MemorySafe(options);

Connection and timeout settings

Option Meaning
MaxConnectionsPerHost Per-origin connection cap
MaxTotalConnections Global connection cap across all hosts
MaxIdleConnectionDuration Idle eviction threshold
MaxConnectionLifetime Absolute connection lifetime cap
MaxConnectionWaitTimeout Time to wait for a free pooled connection
ConnectTimeout TCP/TLS/QUIC connect timeout
RequestTimeout End-to-end request timeout
IdleSweepInterval Frequency of idle pool cleanup
ExpectContinueTimeout How long to wait for 100 Continue
TcpNoDelay Enables/disables TCP_NODELAY

Response handling and resilience

Option Meaning
AutomaticDecompression Response decompression flags
MaxResponseHeadersByteSize Hard cap on response header bytes
MaxResponseContentLength Cap for helper-based buffering/materialization paths
AllowAutoRedirect / MaxAutomaticRedirections Redirect behavior
MaxRetries Number of retries
IsRetryable Custom retry predicate
InitialBackoff, MaxBackoff, BackoffMultiplier, JitterFactor, MaxRetryAfter Retry timing controls

Cookies, auth, proxy, and DNS

Option Meaning
CookieContainer Shared cookie jar
CredentialProvider Shared auth provider
ProxyUri / ProxyCredentials Proxy support
DnsResolver Custom DNS resolver
SocketOptions Low-level socket tuning
ConnectionFactoryOverride Full transport override hook

Protocol-specific settings

Option Meaning
Http2KeepAliveInterval / Http2KeepAliveTimeout HTTP/2 ping keepalive
Http3AllowTlsResume Enables TLS 1.3 session resumption for HTTP/3 reconnects
ResponseCache Shared in-memory response cache
EnableServerPush Enables push handling when a cache is configured

Per-request overrides

CosmoRequest supports request-local behavior:

  • Headers
  • CookieContainer
  • CredentialProvider
  • Body
  • BodyStream
  • BodyLength

Helper methods include:

  • SetUri
  • SetTextBody
  • SetBytesBody
  • SetJsonBody
  • SetMultipartBody
  • SetCompressedBody
  • SetBearerToken
  • SetBasicAuth

Extension points

Two pluggable seams on CosmoHttpOptions let you swap implementations without forking the library:

Option Level When to use
ConnectionFactoryOverride : ICosmoConnectionFactory Socket / byte-stream Custom byte-stream transports (Unix-domain sockets, in-process duplex pipes for tests). Default opens TCP and wraps with SslStream.
RequestExecutorOverride : ICosmoRequestExecutor Whole request Bypass the per-host TCP pool entirely. Primary use case is iOS background URLSession — see iOS background requests. The executor produces a CosmoResponse; cookies, decompression, redirect, retry, telemetry, and the response cache still wrap the call.
var options = new CosmoHttpOptions
{
    ConnectionFactoryOverride = new MyUnixSocketFactory(),
    RequestExecutorOverride   = myCustomExecutor,
};

gRPC

The framing primitives (CosmoGrpcFraming, CosmoGrpcResponse<T>, CosmoGrpcStatus, CosmoGrpcException) are wire-compatible with CosmoApiServer's gRPC middleware: 5-byte length prefix + payload, status carried in the grpc-status / percent-encoded grpc-message trailers, te: trailers request header. They stay payload-format-agnostic, so you can plug Google.Protobuf, protobuf-net, MessagePack, or anything else.

The unary-gRPC extension method that previously hung off CosmoHttp2Client was removed when the legacy H2 stack was deleted in P2.8. A FastH2Client-based replacement is on the roadmap; in the meantime, callers can compose FastH2Client.PostAsync with CosmoGrpcFraming directly.

Caching

The repository includes an in-memory RFC 7234-style cache (CosmoResponseCache).

Highlights:

  • GET/HEAD caching
  • freshness and validator handling
  • conditional revalidation with ETag / Last-Modified
  • only-if-cached
  • HTTP/3 pushed responses can populate the same cache

Example:

var cache = new CosmoResponseCache();

await using var client = new CosmoClient(new CosmoHttpOptions
{
    ResponseCache = cache,
    EnableServerPush = true
});

Dependency injection

The DI extensions live under CosmoHttpClient.DependencyInjection.

Register the default client

services.AddCosmoClient(options =>
{
    options.UserAgent = "myapp/1.0";
    options.MaxRetries = 2;
});

Register a named client

services.AddCosmoClient("github", options =>
{
    options.UserAgent = "myapp/1.0";
});

Resolve it through ICosmoClientFactory:

public sealed class GitHubService(ICosmoClientFactory factory)
{
    public async Task<string> GetZenAsync()
    {
        var client = factory.Create("github");
        await using var response = await client.GetAsync("https://api.github.com/zen");
        return await response.ReadBodyAsStringAsync();
    }
}

Register a typed client

services.AddCosmoClient<MyApiClient>(options =>
{
    options.UserAgent = "myapp/1.0";
});

The typed client constructor must take CosmoClient as its first argument.

Telemetry

CosmoTelemetry exposes:

  • ActivitySourceName = "CosmoHttpClient"
  • MeterName = "CosmoHttpClient"

Metrics include:

  • http.client.request.duration
  • http.client.active_requests
  • http.client.request.body.size
  • http.client.response.body.size

Trace context headers are injected when an active W3C activity exists and the request has not already set traceparent / tracestate.

Benchmarks

The benchmark project compares CosmoHttpClient (both legacy and fast surfaces) against Microsoft HttpClient over a loopback Kestrel server. Each suite covers four scenarios: GET plaintext 1 KiB, GET JSON, POST echo 4 KiB, download 64 KiB. The fast surface adds streaming, concurrent multiplex, single-connection, pool, friendly-API, and TLS variants.

For headline fast-surface numbers vs HttpClient, see Performance above. This section retains the legacy-surface comparison for completeness.

BenchmarkDotNet config: 3 warmups + 10 measured iterations ([SimpleJob(warmupCount: 3, iterationCount: 10)]) — confidence intervals are tight enough (Error/Mean typically 1-5%) without 10-minute run times. Apple M1, .NET 10.0.1.

Legacy CosmoClient (HTTP/1.1)

Scenario CosmoClient HttpClient
GET plaintext 1 KiB 50.55 us, 4.19 KB 52.91 us, 3.31 KB
GET JSON 49.99 us, 4.50 KB 53.70 us, 2.68 KB
POST echo 4 KiB 53.16 us, 3.66 KB 53.75 us, 3.24 KB
Download 64 KiB 78.72 us, 68.29 KB 72.86 us, 67.42 KB

Fast HTTP/2 (FastH2Client)

Scenario FastH2Client HttpClient
GET plaintext 1 KiB (h2c) 52.06 us, 1.94 KB 57.11 us, 4.02 KB
GET plaintext 1 KiB (h2 over TLS) 61.97 us, 3.12 KB 65.78 us, 5.27 KB
16-stream concurrent GETs 166.11 us, 28.14 KB 145.85 us, 64.52 KB
100-stream concurrent GETs 650.67 us, 172.93 KB 433.66 us, 401.63 KB
HPACK + frame compose/parse (in-memory) 207.7 ns, 0 B 300.2 ns, 1.70 KB (string-bag baseline)

The fast surface is significantly more allocation-efficient than HttpClient across the board (≈40-60% of HttpClient's per-op allocations). Latency is competitive on small payloads and currently slower on the highest-concurrency stress benchmarks; tracking under the fast-h2-* perf gates.

Benchmark reports (markdown + CSV + HTML) are written under BenchmarkDotNet.Artifacts/results/ after each run.

To re-run a single suite:

# Fast-surface benchmarks
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastDataPlaneBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1ConnectionBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1ConnectionPoolBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1PoolConcurrencyBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastClientBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1TlsBenchmarks*'

# H/2 fast-surface benchmarks
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH2DataPlaneBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH2ConnectionBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH2ClientBenchmarks*'

# Legacy H/1.1 benchmarks (CosmoClient)
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*Http11ClientBenchmarks*'

# Full sweep + perf-gate validation in one shot
dotnet run -c Release --project tools/PerfGate

Platform notes

iOS background requests

When iOS suspends a backgrounded app, in-process TCP I/O dies with The network connection was lost because the runtime is paused. To keep transfers alive while the app is suspended, the OS-level NSURLSession configured for background mode must own the request lifecycle.

CosmoClient exposes the ICosmoRequestExecutor extension point for this — wire-level transports plug at ICosmoConnectionFactory, but NSURLSession is request-shaped and needs the higher-level seam. Set CosmoHttpOptions.RequestExecutorOverride to dispatch every request through your own executor instead of the per-host TCP pool. Cookies, decompression, redirect, retry, telemetry, and the response cache still wrap the call.

var options = new CosmoHttpOptions
{
#if IOS
    RequestExecutorOverride = DeviceInfo.DeviceType == DeviceType.Virtual
        ? null  // iOS Simulator doesn't support background sessions
        : new NSUrlBackgroundRequestExecutor("com.myapp.bg"),
#endif
};
var client = new CosmoClient(options);

The NSURLSession-backed implementation lives in your platform-specific package — Foundation bindings are intentionally not part of this assembly. The full sample below is what you'd drop into a net10.0-ios project (e.g. a MAUI app's iOS head or a CosmoHttpClient.iOS companion package).

Sample NSURLSession bridge
// CosmoHttpClient.iOS/NSUrlBackgroundRequestExecutor.cs
// Target framework: net10.0-ios
using System.Net;
using CosmoHttpClient;
using Foundation;

public sealed class NSUrlBackgroundRequestExecutor : NSObject, ICosmoRequestExecutor, INSUrlSessionDataDelegate
{
    private readonly NSUrlSession _session;

    public NSUrlBackgroundRequestExecutor(string sessionIdentifier)
    {
        // Background sessions persist across app suspension. The OS resumes the
        // transfer in a separate process and wakes the app to deliver the result.
        var cfg = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration(sessionIdentifier);
        cfg.SessionSendsLaunchEvents = true;
        cfg.WaitsForConnectivity = true;
        cfg.HttpMaximumConnectionsPerHost = 6;
        _session = NSUrlSession.FromConfiguration(cfg, (INSUrlSessionDelegate)this, null);
    }

    public async Task<CosmoResponse> SendAsync(CosmoRequest request, CancellationToken ct)
    {
        if (request.Uri is null)
            throw new InvalidOperationException("CosmoRequest.Uri is required.");

        using var nsRequest = new NSMutableUrlRequest(NSUrl.FromString(request.Uri.ToString())!)
        {
            HttpMethod = request.Method.Name,
        };

        // Copy user headers. NSURLSession sets Host / Content-Length / connection
        // headers itself — skip the ones it manages.
        if (request.HeadersOrNull is { } headers)
        {
            using var hdrDict = new NSMutableDictionary();
            foreach (var kv in headers.AsEnumerable())
            {
                if (string.Equals(kv.Key, "Host", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(kv.Key, "Content-Length", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(kv.Key, "Connection", StringComparison.OrdinalIgnoreCase))
                    continue;
                hdrDict[kv.Key] = (NSString)kv.Value;
            }
            nsRequest.Headers = hdrDict;
        }

        // Body. Background sessions accept either an in-memory body or a file-
        // backed upload task. For simplicity, materialize streamed bodies into
        // an NSData; replace with a temp-file upload task for very large bodies.
        if (!request.Body.IsEmpty)
        {
            nsRequest.Body = NSData.FromArray(request.Body.ToArray());
        }
        else if (request.BodyStream is not null)
        {
            using var ms = new MemoryStream();
            await request.BodyStream.CopyToAsync(ms, ct).ConfigureAwait(false);
            nsRequest.Body = NSData.FromArray(ms.ToArray());
        }

        // Run the data task. CreateDataTaskAsync awaits both completion AND any
        // background-resume cycle that happened while the app was suspended.
        using var ctRegistration = ct.Register(() => { /* see note below */ });
        var (data, nsResponse) = await _session.CreateDataTaskAsync(nsRequest).ConfigureAwait(false);
        ct.ThrowIfCancellationRequested();

        var http = (NSHttpUrlResponse)nsResponse;
        var response = new CosmoResponse
        {
            StatusCode = (int)http.StatusCode,
            ReasonPhrase = ((HttpStatusCode)http.StatusCode).ToString(),
        };

        foreach (var kv in http.AllHeaderFields)
        {
            string name = kv.Key.ToString();
            string value = kv.Value.ToString();
            // Cosmo wraps decompression itself; let it see the original
            // Content-Encoding header.
            response.Headers.Add(name, value);
        }

        // Body. NSData byte[] is owned by Foundation; copy out so we can dispose
        // the NSData without holding the bytes hostage.
        response.Body = data is null || data.Length == 0
            ? Stream.Null
            : new MemoryStream(data.ToArray(), writable: false);

        return response;
    }

    // Cancellation: NSURLSessionDataTask exposes Cancel(); wire it through ct
    // by holding the task reference and cancelling on the registration callback.
    // CreateDataTaskAsync hides the task — for production cancellation support,
    // call CreateDataTask(...) directly, register ct => task.Cancel(), and
    // bridge the callback delegate to a TaskCompletionSource.
}

Wire it up in MAUI's MauiProgram:

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder().UseMauiApp<App>();

    builder.Services.AddSingleton<CosmoClient>(_ =>
    {
        var options = new CosmoHttpOptions
        {
#if IOS
            RequestExecutorOverride = DeviceInfo.DeviceType == DeviceType.Virtual
                ? null
                : new NSUrlBackgroundRequestExecutor("com.myapp.bg"),
#endif
        };
        return new CosmoClient(options);
    });

    return builder.Build();
}

Add the AppDelegate hook so iOS can hand background-session events back to your session:

// AppDelegate.cs (iOS head)
public partial class AppDelegate : MauiUIApplicationDelegate
{
    private static readonly Dictionary<string, Action> _backgroundCompletionHandlers = new();

    [Export("application:handleEventsForBackgroundURLSession:completionHandler:")]
    public override void HandleEventsForBackgroundUrl(
        UIApplication application,
        string sessionIdentifier,
        Action completionHandler)
    {
        // Stash the completion handler keyed by the session id; your delegate's
        // DidFinishEventsForBackgroundSession callback invokes it when iOS has
        // delivered every queued event for that session.
        _backgroundCompletionHandlers[sessionIdentifier] = completionHandler;
    }

    internal static void InvokeBackgroundCompletion(string sessionIdentifier)
    {
        if (_backgroundCompletionHandlers.Remove(sessionIdentifier, out var handler))
            handler();
    }
}

Inside your INSUrlSessionDelegate.DidFinishEventsForBackgroundSession callback, call AppDelegate.InvokeBackgroundCompletion(session.Configuration.Identifier). iOS will then mark the app fully resumed.

Notes:

  • The simulator doesn't support background URL sessions — fall back to the default Cosmo TCP path on virtual devices, as the snippet shows.
  • Keep one executor instance per session identifier for the app's lifetime; recreating it abandons in-flight transfers.
  • Background transfers are best-effort and slower than foreground ones (iOS schedules them opportunistically). Use this for "must-succeed" requests, not chatty UI calls.
  • For large uploads, swap CreateDataTaskAsync for CreateUploadTask(NSUrl fileUrl) — Foundation requires a file-backed body for true background uploads.

Limitations and behavior notes

  • CosmoClient supports http:// and https:// only.
  • CosmoWebSocket supports ws:// and wss://.
  • MaxResponseContentLength limits helper-based buffering/materialization paths, not manual streaming from response.Body.
  • Redirects strip Authorization and Proxy-Authorization on cross-origin hops.
  • CosmoPipelineClient should only be used against trusted origins with known pipelining support.
  • HTTP/3 depends on runtime/platform QUIC support.
  • The repository supports HTTP/3 TLS session resumption, but not a custom public 0-RTT early-data send API.
  • Request compression is explicit; the client does not auto-compress outbound bodies.
  • gRPC support is unary only for now — server-streaming, client-streaming, and bidi-streaming RPCs are not yet implemented. Compressed gRPC frames (compression flag 1) are rejected with CosmoGrpcStatus.Internal; configure your server to send identity (uncompressed) gRPC payloads.
  • iOS background URLSession integration is available via ICosmoRequestExecutor, but the Foundation-binding implementation lives in your iOS-targeted package — this assembly stays platform-neutral and ships only the seam.

Development notes

Common commands:

dotnet build --nologo
dotnet test --nologo --no-build
dotnet run --project samples/CosmoHttpClient.Sample
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks

The current test suite is xUnit-based and exercises HTTP/1.1, HTTP/2, HTTP/3, websockets, caching, DI, auth, resiliency, and telemetry.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on CosmoHttpClient:

Package Downloads
CosmoBlob.Client

CosmoHttpClient-backed client for CosmoKvD's /blobs/{key} surface. Drop into any .NET 10 app that wants an opaque blob store; pair with a thin IBlobStore adapter when used with CosmoMailServer. v1.0.0 swaps System.Net.Http.HttpClient for CosmoHttpClient's FastAutoClient (HTTP/1.1+2+3 auto-negotiation, span-based hot path).

CosmoMail

Lightweight .NET SMTP and IMAP client library with MIME generation, templating, attachments, inline images, and STARTTLS support.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.1.1 90 5/29/2026
5.1.0 803 5/24/2026
5.0.5 242 5/21/2026
5.0.4 89 5/21/2026
5.0.3 101 5/18/2026
5.0.2 109 5/18/2026
5.0.1 89 5/18/2026
5.0.0 104 5/10/2026
4.0.0 104 5/10/2026
3.0.0 99 5/9/2026
2.0.0 96 5/9/2026
1.0.0 98 5/8/2026