Skip to content

ioxide: subscribe to async, and retune the profile to 16k/5ms - #1341

Merged
MDA2AV merged 30 commits into
mainfrom
feat/ioxide-async-delay
Aug 28, 2026
Merged

ioxide: subscribe to async, and retune the profile to 16k/5ms#1341
MDA2AV merged 30 commits into
mainfrom
feat/ioxide-async-delay

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Subscribes the ioxide entry to the async profile, and retunes the profile itself. Both are here because #1342 was folded in on request.

The entry

ReactorDelay gives the profile's wait a home that does not drag the connection off the reactor that owns it. Two mechanisms, selectable with IOXIDE_DELAY_MODE:

  • ring (default) - a timerfd armed per request and submitted to the reactor's own ring via SubmitRead, so the kernel holds the deadline and the completion arrives on the reactor thread. This is how ioxide.pg already waits on the Postgres socket.
  • tick - a thread-static priority queue drained from the reactor's hot path, with a sleeping fallback tick for reactors whose connections are all parked.

Measured on the bench hardware at 16,000 connections and 5ms:

rps cpu avg p99 p99.9
tick 1,842,424 3435%
ring 2,462,698 5831% 6.48ms 9.91ms 16.50ms
tokio 2,054,680 3320%

The bugs found along the way, each measured rather than guessed:

  • A process-global lock taken per request. Register ran on every call. Worth +48% on the bench box (1,059,674 to 1,568,519), and it scales with reactor count, which is why a 32-reactor box only showed +13%.
  • A completion allocated per request. A TaskCompletionSource and its Task was 354 MB/s of garbage, driving ~12.5ms gen0 pauses against a 15ms delay. Pooled onto the session; p99.9 halved.
  • A tick thread spinning a whole core. SpinOnce(-1) never yields, so it burned 87% of a core whether it posted anything or not - the busiest thread in the process. Now below 1%.
  • The ring resuming before its deadline. Not EAGAIN as expected: the fd is reused per connection, so a read could pick up an earlier request's expiration and return a valid 8 bytes. ~0.1% of waits, worst case 14.9ms into a 15ms delay. The deadline now decides, and an early completion goes back on the ring.

Things tried and reverted because they did not pay: RecvSlots 256 to 1024, and RingEntries/ListenBacklog/PoolMax/RecvQueueEntries, none of which moved anything beyond the run-to-run spread.

The profile

64,000 connections at 15ms becomes 16,000 at 5ms. Instrumenting the timer against its own deadline showed it completing 3.2ms late against 23ms of end-to-end overhead, so at 64k the profile was reporting mostly the read/write path rather than the wait. The gap between entries narrowed from 44% to 11.5% on the smaller shape.

Ceiling moves 4.27M to 3.20M. Against a best-so-far of 2.24M that is real headroom but less than before, and the docs now say so instead of claiming the ceiling does not bind.

Touches profiles.sh, the gcannon raw template, the CATALOG row, both profile docs and the README table. Generator runs clean, badge parity holds at 579 ranks.

Existing async-64000 results are orphaned by the retune, so the column is empty until the profile is re-run for all five subscribers. The results committed here predate the retune.

@MDA2AV

MDA2AV commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

/benchmark --save

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: ioxide | Test: all tests

Test Conn RPS CPU Mem Δ RPS Δ Mem
baseline 512 3,864,997 5891.6% 297MiB ~0% ~0%
baseline 4096 3,896,541 5983.6% 461MiB ~0% ~0%
pipelined 512 51,838,569 6060.3% 262MiB ~0% ~0%
pipelined 4096 50,917,974 6012.5% 450MiB ~0% ~0%
limited-conn 512 2,484,028 5261.2% 367MiB ~0% ~0%
limited-conn 4096 2,535,800 5542.1% 496MiB ~0% ~0%
json 4096 1,725,815 5923.9% 864MiB ~0% ~0%
json-comp 512 934,779 5853.6% 626MiB ~0% ~0%
json-comp 4096 904,390 6095.3% 1.7GiB ~0% ~0%
json-comp 16384 909,793 6009.9% 5.7GiB ~0% ~0%
json-tls 4096 1,773,448 6302.8% 1.2GiB ~0% ~0%
upload 32 2,410 1914.2% 464MiB ~0% ~0%
upload 256 2,404 4884.7% 574MiB ~0% ~0%
static 1024 912,337 6157.3% 385MiB ~0% ~0%
static 4096 901,169 6396.7% 702MiB ~0% ~0%
static 6800 867,010 6565.5% 973MiB ~0% ~0%
static-tls 1024 1,081,269 6108.5% 500MiB ~0% ~0%
static-tls 4096 770,758 6300.3% 1.1GiB ~0% ~0%
static-tls 6800 665,645 5920.7% 1.6GiB ~0% ~0%
async-db 1024 354,885 2971.9% 434MiB ~0% ~0%
crud 4096 798,186 3334.8% 711MiB ~0% ~0%
baseline-h2 256 10,329,244 4517.9% 453MiB ~0% ~0%
baseline-h2 1024 11,556,640 5180.6% 721MiB ~0% ~0%
static-h2 256 1,131,774 5744.3% 580MiB ~0% ~0%
static-h2 1024 923,633 5864.7% 1.3GiB ~0% ~0%
baseline-h3 64 3,787,873 3234.8% 268MiB ~0% ~0%
static-h3 64 536,259 3325.2% 688MiB ~0% ~0%
latency-1m 1024 997,644 2126.2% 268MiB ~0% ~0%
async 64000 1,059,674 2411.8% 2.2GiB ~0% ~0%
Full log
requests: 2664742 total, 2668838 started, 2664742 done, 2664742 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 2664787 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 40.42GB (43398646599) total, 25.80MB (27049023) headers (space savings 84.41%), 40.38GB (43358611247) data
UDP datagram: 242685 sent, 32666429 received
                 min         max         median     p95        p99        mean         sd        +/- sd
request     :     1.12ms     73.77ms      7.31ms    17.47ms    22.06ms     8.80ms      4.24ms    78.25%
connect     :     1.77ms     20.83ms      2.60ms     6.33ms    20.83ms     3.24ms      2.85ms    93.75%
TTFB        :     3.12ms     25.48ms      4.78ms    20.80ms    25.48ms     7.87ms      5.94ms    84.38%
req/s       :    3420.69    11682.68     8114.68   11344.77   11682.68    8326.35     2668.32    57.81%
min RTT     :       68us      1.17ms       249us     1.04ms     1.17ms      413us       294us    82.81%
smoothed RTT:      435us      2.40ms       884us     2.03ms     2.40ms     1.05ms       468us    76.56%
packets sent:       1523        5710        3568       5284       5710    3793.95     1286.17    50.00%
packets recv:     210327      715970      497301     695266     715970  510414.95   163409.22    57.81%
packets lost:          0           0           0          0          0       0.00        0.00   100.00%
GRO packets :          1          44           7         12         12       7.29        4.10    42.69%
[info] CPU 3659.6% | Mem 807MiB

=== Best: 536259 req/s (CPU: 3325.2%, Mem: 688MiB) ===
[info] saved results/static-h3/64/ioxide.json
httparena-bench-ioxide
httparena-bench-ioxide
[info] skip: ioxide does not subscribe to gateway-64
[info] skip: ioxide does not subscribe to gateway-h3
[info] skip: ioxide does not subscribe to production-stack
[info] skip: ioxide does not subscribe to unary-grpc
[info] skip: ioxide does not subscribe to unary-grpc-tls
[info] skip: ioxide does not subscribe to echo-ws
[info] skip: ioxide does not subscribe to echo-ws-pipeline
[info] skip: ioxide does not subscribe to echo-ws-limited

==============================================
=== ioxide / latency-1m / 1024c (tool=zrk) ===
==============================================
[info] waiting for server...
[info] server ready

[run 1/3]
{
  "zrk_version": "2.3.0",
  "target": { "url": "http://localhost:8080/baseline11?a=1&b=2", "method": "GET" },
  "config": { "connections": 1024, "launched": 1024, "duration_s": 20.000, "closed": false, "target_rate": 1000000, "timeout_ms": 2000, "deadline_ms": 0, "deadline_abort": false, "record_timeouts": true },
  "duration_s": 20.005,
  "requests": 19956815,
  "bytes": 1297192975,
  "achieved_rate": 997589.78,
  "target_rate": 1000000,
  "target_rate_end": 1000000,
  "rate_ratio": 0.9976,
  "bytes_per_sec": 64843335.92,
  "error_rate": 0.000000,
  "max_schedule_lag_us": 149208,
  "latency_us": {
    "min": 23, "mean": 455.7, "stdev": 4560.5, "max": 150271,
    "p50": 77, "p75": 96, "p90": 125, "p99": 1350, "p99_9": 77088, "p99_99": 109088
  },
  "status_codes": { "1xx": 0, "2xx": 19956815, "3xx": 0, "4xx": 0, "5xx": 0 },
  "errors": { "connect": 0, "read": 0, "write": 0, "timeout": 0, "deadline": 0, "non_2xx_3xx": 0 },
  "latency_histogram": "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"
}
[info] CPU 2131.5% | Mem 268MiB

[run 2/3]
{
  "zrk_version": "2.3.0",
  "target": { "url": "http://localhost:8080/baseline11?a=1&b=2", "method": "GET" },
  "config": { "connections": 1024, "launched": 1024, "duration_s": 20.000, "closed": false, "target_rate": 1000000, "timeout_ms": 2000, "deadline_ms": 0, "deadline_abort": false, "record_timeouts": true },
  "duration_s": 20.004,
  "requests": 19956452,
  "bytes": 1297169380,
  "achieved_rate": 997643.95,
  "target_rate": 1000000,
  "target_rate_end": 1000000,
  "rate_ratio": 0.9976,
  "bytes_per_sec": 64846856.81,
  "error_rate": 0.000000,
  "max_schedule_lag_us": 56106,
  "latency_us": {
    "min": 23, "mean": 116.1, "stdev": 502.4, "max": 57151,
    "p50": 78, "p75": 96, "p90": 123, "p99": 959, "p99_9": 3661, "p99_99": 25480
  },
  "status_codes": { "1xx": 0, "2xx": 19956452, "3xx": 0, "4xx": 0, "5xx": 0 },
  "errors": { "connect": 0, "read": 0, "write": 0, "timeout": 0, "deadline": 0, "non_2xx_3xx": 0 },
  "latency_histogram": "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"
}
[info] CPU 2126.2% | Mem 268MiB

[run 3/3]
{
  "zrk_version": "2.3.0",
  "target": { "url": "http://localhost:8080/baseline11?a=1&b=2", "method": "GET" },
  "config": { "connections": 1024, "launched": 1024, "duration_s": 20.000, "closed": false, "target_rate": 1000000, "timeout_ms": 2000, "deadline_ms": 0, "deadline_abort": false, "record_timeouts": true },
  "duration_s": 20.004,
  "requests": 19956284,
  "bytes": 1297158460,
  "achieved_rate": 997625.33,
  "target_rate": 1000000,
  "target_rate_end": 1000000,
  "rate_ratio": 0.9976,
  "bytes_per_sec": 64845646.48,
  "error_rate": 0.000000,
  "max_schedule_lag_us": 63526,
  "latency_us": {
    "min": 23, "mean": 122.1, "stdev": 658.2, "max": 64575,
    "p50": 79, "p75": 98, "p90": 125, "p99": 940, "p99_9": 5698, "p99_99": 34224
  },
  "status_codes": { "1xx": 0, "2xx": 19956284, "3xx": 0, "4xx": 0, "5xx": 0 },
  "errors": { "connect": 0, "read": 0, "write": 0, "timeout": 0, "deadline": 0, "non_2xx_3xx": 0 },
  "latency_histogram": "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"
}
[info] CPU 2124.9% | Mem 304MiB
[info] run 2 wins on score

=== Best: 997644 req/s (CPU: 2126.2%, Mem: 268MiB) ===
[info] exact CPU: 387.94 core-seconds | 19.439 us/req
[info] saved results/latency-1m/1024/ioxide.json
httparena-bench-ioxide
httparena-bench-ioxide

==============================================
=== ioxide / async / 64000c (tool=gcannon) ===
==============================================
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   44.81ms   43.70ms   61.30ms   82.20ms   102.20ms

  10022763 requests in 10.00s, 9958763 responses
  Throughput: 995.66K req/s
  Bandwidth:  62.67MB/s
  Status codes: 2xx=9958763, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 9958763 / 9958763 responses (100.0%)
[info] CPU 2252.9% | Mem 2.0GiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   43.50ms   42.70ms   59.40ms   75.30ms   89.00ms

  10359814 requests in 10.00s, 10295814 responses
  Throughput: 1.03M req/s
  Bandwidth:  64.79MB/s
  Status codes: 2xx=10295814, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 10295814 / 10295814 responses (100.0%)
[info] CPU 2350.1% | Mem 2.2GiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   42.28ms   41.70ms   58.80ms   74.70ms   87.80ms

  10660740 requests in 10.00s, 10596740 responses
  Throughput: 1.06M req/s
  Bandwidth:  66.69MB/s
  Status codes: 2xx=10596740, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 10596740 / 10596740 responses (100.0%)
[info] CPU 2411.8% | Mem 2.2GiB

=== Best: 1059674 req/s (CPU: 2411.8%, Mem: 2.2GiB) ===
[info] input BW: 48.51MB/s (avg template: 48 bytes)
[info] saved results/async/64000/ioxide.json
httparena-bench-ioxide
httparena-bench-ioxide
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/results/ioxide.json - 29 new, 29 total
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

@MDA2AV
MDA2AV force-pushed the feat/ioxide-async-delay branch from cf7ef53 to 66bd8c3 Compare August 26, 2026 22:29
@MDA2AV
MDA2AV changed the base branch from fix/ioxide-static-and-bump to main August 26, 2026 22:30
@MDA2AV

MDA2AV commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

/benchmark -f ioxide -t async

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: ioxide | Test: async

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 64000 1,546,810 3095.9% 2.5GiB +46.0% +13.6%
Full log
[info] available CPUs: 128
[info] framework: ioxide (ioxide, C#)
[info] subscribed tests: baseline,async,latency-1m,pipelined,limited-conn,json,json-comp,json-tls,static,static-tls,upload,async-db,crud,baseline-h2,static-h2,baseline-h3,static-h3
[info] building image: httparena-ioxide
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 631B done
#1 DONE 0.0s

#2 [internal] load metadata for mcr.microsoft.com/dotnet/sdk:11.0-preview
#2 DONE 0.3s

#3 [internal] load metadata for mcr.microsoft.com/dotnet/runtime:11.0-preview
#3 DONE 0.3s

#4 [internal] load .dockerignore
#4 transferring context: 50B done
#4 DONE 0.0s

#5 [internal] load build context
#5 DONE 0.0s

#6 [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9
#6 resolve mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9 0.1s done
#6 DONE 0.1s

#7 [stage-1 1/3] FROM mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda
#7 resolve mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda 0.1s done
#7 DONE 0.1s

#5 [internal] load build context
#5 transferring context: 95.62kB done
#5 DONE 0.0s

#8 [build 2/6] WORKDIR /source
#8 CACHED

#9 [build 3/6] COPY ioxide-arena.csproj ./
#9 CACHED

#10 [build 4/6] RUN dotnet restore
#10 CACHED

#11 [build 5/6] COPY . .
#11 DONE 0.2s

#12 [build 6/6] RUN dotnet publish -c Release --no-self-contained -o /app/out
#12 1.470   Determining projects to restore...
#12 1.748   All projects are up-to-date for restore.
#12 1.867 /usr/share/dotnet/sdk/11.0.100-preview.7.26381.103/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.RuntimeIdentifierInference.targets(385,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [/source/ioxide-arena.csproj]
#12 3.627   ioxide-arena -> /source/bin/Release/net11.0/ioxide-arena.dll
#12 3.640   ioxide-arena -> /app/out/
#12 DONE 3.7s

#13 [stage-1 2/3] WORKDIR /app
#13 CACHED

#14 [stage-1 3/3] COPY --from=build /app/out ./
#14 DONE 0.1s

#15 exporting to image
#15 exporting layers
#15 exporting layers 0.3s done
#15 exporting manifest sha256:483445bdf96d42f049aee7190571904d518e1f5b71569d2a5f81b8e4555e536f 0.0s done
#15 exporting config sha256:9791e800dd51ae4fdf7f23865cc9a2e1a3b9a5acee72273c13c8fb8bc4950673 0.0s done
#15 exporting attestation manifest sha256:db07bd6d9441d57c85b028ce0e534a2a711c7560b5dd9640416addb27efbe6d3 0.1s done
#15 exporting manifest list sha256:28fef2acd4e45d464fb3744404bc70c28d9685cf2a6cf9968a8325aa87b29089
#15 exporting manifest list sha256:28fef2acd4e45d464fb3744404bc70c28d9685cf2a6cf9968a8325aa87b29089 0.0s done
#15 naming to docker.io/library/httparena-ioxide:latest done
#15 unpacking to docker.io/library/httparena-ioxide:latest 0.1s done
#15 DONE 0.6s
[info] zrk: docker mode (zrk:local)
[info] tuning host for benchmark runs
[info] CPU governor → performance
[info] setting kernel socket limits
[info] setting UDP buffer sizes for QUIC
[info] setting loopback MTU to 1500 (realistic Ethernet)
[info] restarting docker daemon
[info] dropping kernel caches
[info] starting postgres sidecar
[info] postgres ready (seeded)
[info] starting redis sidecar (cpuset=0,64)
[info] redis ready

==============================================
=== ioxide / async / 64000c (tool=gcannon) ===
==============================================
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   30.00ms   29.50ms   32.60ms   74.80ms   174.60ms

  15053519 requests in 10.00s, 14989519 responses
  Throughput: 1.50M req/s
  Bandwidth:  94.33MB/s
  Status codes: 2xx=14989519, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 14989519 / 14989519 responses (100.0%)
[info] CPU 3014.7% | Mem 1.8GiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   28.87ms   29.80ms   32.20ms   46.10ms   54.10ms

  15532103 requests in 10.00s, 15468103 responses
  Throughput: 1.55M req/s
  Bandwidth:  97.34MB/s
  Status codes: 2xx=15468103, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15468103 / 15468103 responses (100.0%)
[info] CPU 3095.9% | Mem 2.5GiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.44ms   29.90ms   32.30ms   47.90ms   129.60ms

  15245141 requests in 10.00s, 15181141 responses
  Throughput: 1.52M req/s
  Bandwidth:  95.53MB/s
  Status codes: 2xx=15181141, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15181141 / 15181141 responses (100.0%)
[info] CPU 3019.8% | Mem 3.0GiB

=== Best: 1546810 req/s (CPU: 3095.9%, Mem: 2.5GiB) ===
[info] input BW: 70.81MB/s (avg template: 48 bytes)
[info] saved results/async/64000/ioxide.json
httparena-bench-ioxide
httparena-bench-ioxide
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/results/ioxide.json - 1 new, 29 total
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

@MDA2AV

MDA2AV commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

/benchmark -f ioxide -t async

Measuring e6c2e140 (drains on the reactor hot path, sleeping fallback tick, pooled completion). Not saving, this is to see whether CPU utilisation moves off 3000% of the available 6400%.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: ioxide | Test: async

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 64000 1,592,007 2895.9% 1.9GiB +50.2% -13.6%
Full log
[info] available CPUs: 128
[info] framework: ioxide (ioxide, C#)
[info] subscribed tests: baseline,async,latency-1m,pipelined,limited-conn,json,json-comp,json-tls,static,static-tls,upload,async-db,crud,baseline-h2,static-h2,baseline-h3,static-h3
[info] building image: httparena-ioxide
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 631B done
#1 DONE 0.0s

#2 [internal] load metadata for mcr.microsoft.com/dotnet/runtime:11.0-preview
#2 ...

#3 [internal] load metadata for mcr.microsoft.com/dotnet/sdk:11.0-preview
#3 DONE 0.4s

#2 [internal] load metadata for mcr.microsoft.com/dotnet/runtime:11.0-preview
#2 DONE 0.4s

#4 [internal] load .dockerignore
#4 transferring context: 50B done
#4 DONE 0.0s

#5 [internal] load build context
#5 DONE 0.0s

#6 [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9
#6 resolve mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9 0.1s done
#6 DONE 0.1s

#7 [stage-1 1/3] FROM mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda
#7 resolve mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda 0.1s done
#7 DONE 0.1s

#5 [internal] load build context
#5 transferring context: 53.91kB done
#5 DONE 0.0s

#8 [build 2/6] WORKDIR /source
#8 CACHED

#9 [build 3/6] COPY ioxide-arena.csproj ./
#9 CACHED

#10 [build 4/6] RUN dotnet restore
#10 CACHED

#11 [build 5/6] COPY . .
#11 DONE 0.1s

#12 [build 6/6] RUN dotnet publish -c Release --no-self-contained -o /app/out
#12 1.287   Determining projects to restore...
#12 1.565   All projects are up-to-date for restore.
#12 1.685 /usr/share/dotnet/sdk/11.0.100-preview.7.26381.103/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.RuntimeIdentifierInference.targets(385,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [/source/ioxide-arena.csproj]
#12 3.365   ioxide-arena -> /source/bin/Release/net11.0/ioxide-arena.dll
#12 3.389   ioxide-arena -> /app/out/
#12 DONE 3.5s

#13 [stage-1 2/3] WORKDIR /app
#13 CACHED

#14 [stage-1 3/3] COPY --from=build /app/out ./
#14 DONE 0.1s

#15 exporting to image
#15 exporting layers
#15 exporting layers 0.3s done
#15 exporting manifest sha256:3e7004958a557197f06f74717f3b4053263b201d8ea01edf0116fa174bb1537c 0.0s done
#15 exporting config sha256:30db1fc503ce4b7fd4a7130573f81272b3ff10fe563dda14aeb8fac6bd779b2e 0.0s done
#15 exporting attestation manifest sha256:6809e414108f76bfa346a11baacd7d3502ceb0502f41d031e85b9b523dbacdfb 0.0s done
#15 exporting manifest list sha256:20105105e9adce879faf4d4c0938487f443d24219e8f387fe97c44f0cf75845d
#15 exporting manifest list sha256:20105105e9adce879faf4d4c0938487f443d24219e8f387fe97c44f0cf75845d 0.0s done
#15 naming to docker.io/library/httparena-ioxide:latest done
#15 unpacking to docker.io/library/httparena-ioxide:latest 0.1s done
#15 DONE 0.5s
[info] zrk: docker mode (zrk:local)
[info] tuning host for benchmark runs
[info] CPU governor → performance
[info] setting kernel socket limits
[info] setting UDP buffer sizes for QUIC
[info] setting loopback MTU to 1500 (realistic Ethernet)
[info] restarting docker daemon
[info] dropping kernel caches
[info] starting postgres sidecar
[info] postgres ready (seeded)
[info] starting redis sidecar (cpuset=0,64)
[info] redis ready

==============================================
=== ioxide / async / 64000c (tool=gcannon) ===
==============================================
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   28.41ms   29.10ms   32.00ms   41.00ms   92.90ms

  15984073 requests in 10.00s, 15920073 responses
  Throughput: 1.59M req/s
  Bandwidth:  100.18MB/s
  Status codes: 2xx=15920073, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15920073 / 15920073 responses (100.0%)
[info] CPU 2895.9% | Mem 1.9GiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   28.95ms   29.50ms   32.10ms   41.70ms   85.20ms

  15678350 requests in 10.00s, 15614350 responses
  Throughput: 1.56M req/s
  Bandwidth:  98.26MB/s
  Status codes: 2xx=15614350, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15614350 / 15614350 responses (100.0%)
[info] CPU 2795.1% | Mem 2.4GiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.20ms   29.70ms   32.60ms   42.80ms   79.10ms

  15527779 requests in 10.00s, 15463779 responses
  Throughput: 1.55M req/s
  Bandwidth:  97.31MB/s
  Status codes: 2xx=15463779, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15463779 / 15463779 responses (100.0%)
[info] CPU 2779.1% | Mem 2.9GiB

=== Best: 1592007 req/s (CPU: 2895.9%, Mem: 1.9GiB) ===
[info] input BW: 72.88MB/s (avg template: 48 bytes)
[info] saved results/async/64000/ioxide.json
httparena-bench-ioxide
httparena-bench-ioxide
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/results/ioxide.json - 1 new, 29 total
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

@MDA2AV

MDA2AV commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

/benchmark -f ioxide -t async

Recv slots 256 -> 1024 per reactor. The timer instrumentation showed it completes only 3.2ms late on average against a 37.8ms end-to-end latency, so ~20ms is the read/write path, not the wait. This tests whether slot starvation is that 20ms.

Not saving. Watch CPU: it sat at 3000% of an available 6400%, so if this is the limiter, utilisation should rise. Memory will go up (~1GB of recv buffers vs 256MB), which is the trade being measured.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: ioxide | Test: async

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 64000 1,568,519 2802.0% 2.0GiB +48.0% -9.1%
Full log
[info] available CPUs: 128
[info] framework: ioxide (ioxide, C#)
[info] subscribed tests: baseline,async,latency-1m,pipelined,limited-conn,json,json-comp,json-tls,static,static-tls,upload,async-db,crud,baseline-h2,static-h2,baseline-h3,static-h3
[info] building image: httparena-ioxide
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 631B 0.0s done
#1 DONE 0.1s

#2 [internal] load metadata for mcr.microsoft.com/dotnet/sdk:11.0-preview
#2 ...

#3 [internal] load metadata for mcr.microsoft.com/dotnet/runtime:11.0-preview
#3 DONE 0.4s

#2 [internal] load metadata for mcr.microsoft.com/dotnet/sdk:11.0-preview
#2 DONE 0.4s

#4 [internal] load .dockerignore
#4 transferring context: 50B done
#4 DONE 0.0s

#5 [internal] load build context
#5 DONE 0.0s

#6 [stage-1 1/3] FROM mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda
#6 resolve mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda
#6 resolve mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda 0.1s done
#6 DONE 0.1s

#7 [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9
#7 resolve mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9 0.1s done
#7 DONE 0.1s

#5 [internal] load build context
#5 transferring context: 14.66kB done
#5 DONE 0.1s

#8 [build 3/6] COPY ioxide-arena.csproj ./
#8 CACHED

#9 [build 2/6] WORKDIR /source
#9 CACHED

#10 [build 4/6] RUN dotnet restore
#10 CACHED

#11 [build 5/6] COPY . .
#11 DONE 0.2s

#12 [build 6/6] RUN dotnet publish -c Release --no-self-contained -o /app/out
#12 2.627   Determining projects to restore...
#12 2.944   All projects are up-to-date for restore.
#12 3.065 /usr/share/dotnet/sdk/11.0.100-preview.7.26381.103/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.RuntimeIdentifierInference.targets(385,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [/source/ioxide-arena.csproj]
#12 5.505   ioxide-arena -> /source/bin/Release/net11.0/ioxide-arena.dll
#12 5.535   ioxide-arena -> /app/out/
#12 DONE 5.6s

#13 [stage-1 2/3] WORKDIR /app
#13 CACHED

#14 [stage-1 3/3] COPY --from=build /app/out ./
#14 DONE 0.2s

#15 exporting to image
#15 exporting layers
#15 exporting layers 0.5s done
#15 exporting manifest sha256:5f59862ffa44f899c54d00aa573c2bd2b393b59dddddf9205648fb7a917e0933 0.1s done
#15 exporting config sha256:0fed2c2c29f6b392f34cbf3f9bf4a9e89cdd2874968a63e7e1854d6855eb2035 0.1s done
#15 exporting attestation manifest sha256:e893c714252d66e8c4bb1540ed58e76c98d0144614e1307c8446aad60aa71f64 0.1s done
#15 exporting manifest list sha256:e79a5aecd99a46c15b06254fcc8d12fb91c6403eb50e4017bad6ddfbca5ff025
#15 exporting manifest list sha256:e79a5aecd99a46c15b06254fcc8d12fb91c6403eb50e4017bad6ddfbca5ff025 0.1s done
#15 naming to docker.io/library/httparena-ioxide:latest done
#15 unpacking to docker.io/library/httparena-ioxide:latest 0.1s done
#15 DONE 0.9s
[info] zrk: docker mode (zrk:local)
[info] tuning host for benchmark runs
[info] CPU governor → performance
[info] setting kernel socket limits
[info] setting UDP buffer sizes for QUIC
[info] setting loopback MTU to 1500 (realistic Ethernet)
[info] restarting docker daemon
[info] dropping kernel caches
[info] starting postgres sidecar
[info] postgres ready (seeded)
[info] starting redis sidecar (cpuset=0,64)
[info] redis ready

==============================================
=== ioxide / async / 64000c (tool=gcannon) ===
==============================================
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   28.90ms   29.40ms   32.20ms   41.70ms   96.00ms

  15749193 requests in 10.00s, 15685193 responses
  Throughput: 1.57M req/s
  Bandwidth:  98.71MB/s
  Status codes: 2xx=15685193, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15685193 / 15685193 responses (100.0%)
[info] CPU 2802.0% | Mem 2.0GiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.50ms   29.80ms   32.80ms   44.90ms   97.10ms

  15357260 requests in 10.00s, 15293260 responses
  Throughput: 1.53M req/s
  Bandwidth:  96.24MB/s
  Status codes: 2xx=15293260, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15293260 / 15293260 responses (100.0%)
[info] CPU 2772.0% | Mem 2.4GiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.18ms   29.50ms   32.60ms   46.30ms   94.30ms

  15467251 requests in 10.00s, 15403251 responses
  Throughput: 1.54M req/s
  Bandwidth:  96.93MB/s
  Status codes: 2xx=15403251, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15403251 / 15403251 responses (100.0%)
[info] CPU 2787.6% | Mem 2.5GiB

=== Best: 1568519 req/s (CPU: 2802.0%, Mem: 2.0GiB) ===
[info] input BW: 71.80MB/s (avg template: 48 bytes)
[info] saved results/async/64000/ioxide.json
httparena-bench-ioxide
httparena-bench-ioxide
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/results/ioxide.json - 1 new, 29 total
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

@MDA2AV

MDA2AV commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

/benchmark -f ioxide -t async

Delay now defaults to the ring timer: a timerfd armed per request and submitted to the reactor's own ring via SubmitRead, so the kernel holds the deadline and the completion arrives on the reactor thread. No tick thread, no drain, no priority queue.

On my 32-reactor box the tick beat it (1.58M @ 1765% vs 1.50M @ 2007%), because the ring costs a timerfd_settime syscall plus an SQE/CQE per request while the tick batches ~13 timers per drain. But the two costs scale differently: the ring's is per request and flat, the tick's is one post per reactor per tick, so 256k/s at 64 reactors against 128k/s at 32. This reads which side wins on the hardware that matters.

Not saving. Last run was 1,568,519 @ 2802%, with 6400% available.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: ioxide | Test: async

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 64000 1,649,363 4231.3% 2.9GiB +55.6% +31.8%
Full log
[info] available CPUs: 128
[info] framework: ioxide (ioxide, C#)
[info] subscribed tests: baseline,async,latency-1m,pipelined,limited-conn,json,json-comp,json-tls,static,static-tls,upload,async-db,crud,baseline-h2,static-h2,baseline-h3,static-h3
[info] building image: httparena-ioxide
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 631B done
#1 DONE 0.0s

#2 [internal] load metadata for mcr.microsoft.com/dotnet/runtime:11.0-preview
#2 ...

#3 [internal] load metadata for mcr.microsoft.com/dotnet/sdk:11.0-preview
#3 DONE 0.4s

#2 [internal] load metadata for mcr.microsoft.com/dotnet/runtime:11.0-preview
#2 DONE 0.4s

#4 [internal] load .dockerignore
#4 transferring context: 50B done
#4 DONE 0.0s

#5 [internal] load build context
#5 DONE 0.0s

#6 [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9
#6 resolve mcr.microsoft.com/dotnet/sdk:11.0-preview@sha256:b8cc28b501af233e51be59583a41bf3ebde7fc8656418a7dd305b90b21a7f3f9 0.1s done
#6 DONE 0.1s

#7 [stage-1 1/3] FROM mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda
#7 resolve mcr.microsoft.com/dotnet/runtime:11.0-preview@sha256:04373bb9d7b49a83b173e9184e44db5d3acfffb76452ddd70938c9320cf08dda 0.1s done
#7 DONE 0.1s

#5 [internal] load build context
#5 transferring context: 41.83kB done
#5 DONE 0.0s

#8 [build 3/6] COPY ioxide-arena.csproj ./
#8 CACHED

#9 [build 2/6] WORKDIR /source
#9 CACHED

#10 [build 4/6] RUN dotnet restore
#10 CACHED

#11 [build 5/6] COPY . .
#11 DONE 0.1s

#12 [build 6/6] RUN dotnet publish -c Release --no-self-contained -o /app/out
#12 1.292   Determining projects to restore...
#12 1.531   All projects are up-to-date for restore.
#12 1.650 /usr/share/dotnet/sdk/11.0.100-preview.7.26381.103/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.RuntimeIdentifierInference.targets(385,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [/source/ioxide-arena.csproj]
#12 3.330   ioxide-arena -> /source/bin/Release/net11.0/ioxide-arena.dll
#12 3.351   ioxide-arena -> /app/out/
#12 DONE 3.4s

#13 [stage-1 2/3] WORKDIR /app
#13 CACHED

#14 [stage-1 3/3] COPY --from=build /app/out ./
#14 DONE 0.1s

#15 exporting to image
#15 exporting layers
#15 exporting layers 0.3s done
#15 exporting manifest sha256:188d98bd28882df578bc03810bbf3942e918ca9088b10d6457e055a78e1eb983 0.0s done
#15 exporting config sha256:e1a4df83c03998bc647d20897a8ab797cb9c6c335a3e551a34c94a0a2891bde2 0.0s done
#15 exporting attestation manifest sha256:69e9f730f5b9eb84dd237e3fb1e0dcbd3e7834a27e088b32856c86b2db52d408 0.0s done
#15 exporting manifest list sha256:b2056af567483d458578beee98722bda50eed00de3935fa67577c24e25e81318
#15 exporting manifest list sha256:b2056af567483d458578beee98722bda50eed00de3935fa67577c24e25e81318 0.0s done
#15 naming to docker.io/library/httparena-ioxide:latest done
#15 unpacking to docker.io/library/httparena-ioxide:latest 0.1s done
#15 DONE 0.5s
[info] zrk: docker mode (zrk:local)
[info] tuning host for benchmark runs
[info] CPU governor → performance
[info] setting kernel socket limits
[info] setting UDP buffer sizes for QUIC
[info] setting loopback MTU to 1500 (realistic Ethernet)
[info] restarting docker daemon
[info] dropping kernel caches
[info] starting postgres sidecar
[info] postgres ready (seeded)
[info] starting redis sidecar (cpuset=0,64)
[info] redis ready

==============================================
=== ioxide / async / 64000c (tool=gcannon) ===
==============================================
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.06ms   27.70ms   29.80ms   62.60ms   134.90ms

  15637987 requests in 10.00s, 15573987 responses
  Throughput: 1.56M req/s
  Bandwidth:  98.00MB/s
  Status codes: 2xx=15573987, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15573987 / 15573987 responses (100.0%)
[info] CPU 3920.7% | Mem 1.9GiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.31ms   28.10ms   30.20ms   59.10ms   114.60ms

  15354913 requests in 10.00s, 15290913 responses
  Throughput: 1.53M req/s
  Bandwidth:  96.22MB/s
  Status codes: 2xx=15290913, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15290913 / 15290913 responses (100.0%)
[info] CPU 3958.5% | Mem 2.4GiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     64000 (1000/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  10s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   27.19ms   27.40ms   30.10ms   46.10ms   86.60ms

  16557637 requests in 10.00s, 16493637 responses
  Throughput: 1.65M req/s
  Bandwidth:  103.78MB/s
  Status codes: 2xx=16493637, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 16493637 / 16493637 responses (100.0%)
[info] CPU 4231.3% | Mem 2.9GiB

=== Best: 1649363 req/s (CPU: 4231.3%, Mem: 2.9GiB) ===
[info] input BW: 75.50MB/s (avg template: 48 bytes)
[info] saved results/async/64000/ioxide.json
httparena-bench-ioxide
httparena-bench-ioxide
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/results/ioxide.json - 1 new, 29 total
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

MDA2AV added a commit that referenced this pull request Aug 27, 2026
Experiment on the experiment branch; #1341 stays on the tick.

The ring was measured once, at 64,000 connections and 15ms, where it bought 5%
throughput for 51% more CPU and lost. The profile has changed underneath that
result: 16,000 connections at 5ms is a third of the connections and three times
the request rate, and the two mechanisms scale with different things.

The ring costs a timerfd_settime syscall plus an SQE and a CQE per request, so
its cost tracks the request rate and this profile raises that. The tick costs
one post per reactor per tick regardless of the rate, so its cost is unchanged
at 64 reactors while it now amortises over fewer connections per reactor: 250
here against 1,000 before, which is 250 timers per drain becoming about 60.

Both effects point the same way, against the ring, so this is a check rather
than a hope. /delay/5 answers in 5.22-5.28ms on this path.
@MDA2AV MDA2AV changed the title ioxide: subscribe to async, with a delay that stays on the reactor ioxide: subscribe to async, and retune the profile to 16k/5ms Aug 27, 2026
@MDA2AV

MDA2AV commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

/benchmark-multiple -f ioxide,tokio -t async

Consolidated branch: profile at 16,000 conns / 5ms, ioxide on the ring timer with the deadline guard.

The previous 2,462,698 came from the unguarded build, where ~0.1% of waits resumed early (worst case 14.9ms into a 15ms delay). This re-reads it with those requests actually waiting, so the number should be a touch lower and honest.

tokio unchanged as the control: 2,054,680 @ 3319.6% on this profile.

Not saving.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Frameworks: 2 | Test: async

ioxide

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 16000 2,359,158 5558.4% 1.0GiB NEW NEW

tokio

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 16000 2,055,165 3319.4% 609MiB NEW NEW

@MDA2AV

MDA2AV commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

/benchmark-multiple -f ioxide,tokio -t async

Profile now 32,000 connections at 10ms. This is a control, not a retune: the ceiling is connections over delay, so this and 16k/5ms are both 3.20M. Connection count is the only variable.

ceiling ioxide tokio
64,000 / 15ms 4.27M 1,568,519 2,263,962
16,000 / 5ms 3.20M ~2,360,000 2,054,680
32,000 / 10ms 3.20M ? ?

The ordering reverses between the first two, but they differ in ceiling as well as connections so nothing is isolated. Here a drop against the 16k/5ms numbers is connection scaling and cannot be the arithmetic.

ioxide on the ring with the deadline guard. Not saving.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Frameworks: 2 | Test: async

ioxide

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 32000 2,390,257 6066.3% 1.5GiB NEW NEW

tokio

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 32000 2,359,835 4078.8% 1.2GiB NEW NEW

@MDA2AV

MDA2AV commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

/benchmark-multiple -f ioxide,tokio -t async

64,000 connections at 20ms — third point with the ceiling pinned at 3.20M, so connection count stays the only variable.

ceiling ioxide tokio
16,000 / 5ms 3.20M 2,360,000 (74%) 2,054,680 (64%)
32,000 / 10ms 3.20M 2,390,257 (75%) 2,359,835 (74%)
64,000 / 20ms 3.20M ? ?

The first two points already say tokio scales on this axis and ioxide does not: doubling connections moved tokio 2.05M -> 2.36M while ioxide went 2.36M -> 2.39M, flat, needing 6066% CPU against tokio 4079% to stay level.

This point tests something else. The original 64,000 run gave 1,568,519 against a 4.27M ceiling (37%), which we read as connections being the problem. Here the connections are identical and only the arithmetic differs — if this lands near 2.4M, 64,000 connections were never the difficulty.

Caveat: both are already at ~75% of 3.20M and queueing grows fast that close to a ceiling, so some convergence may be the arithmetic rather than the frameworks.

Not saving.

MDA2AV and others added 24 commits August 28, 2026 16:15
The rest of this entry never waits in userspace: ioxide.pg parks on the
Postgres socket through the reactor's ring and the completion arrives on the
reactor thread. The delay was the one wait that did not, so this makes it
possible to do the same and measures what it costs.

Reactor.SubmitRead takes a raw fd, and a timerfd is pollable, so io_uring arms
a poll on it internally rather than handing the read to a worker thread. That
gets to the same place as IORING_OP_TIMEOUT, which ioxide does not expose
(MDA2AV/ioxide#212). One timerfd per connection, created on its first delay and
re-armed after, released with the connection.

It is correct and it is more precise than the tick: /delay/1 answers in 1.06ms,
/delay/200 in 200.23ms, and 30 re-arms on one connection overshoot by 0.18ms on
average against the tick's 0.25-0.82ms.

It is off by default because it measured worse, and the reason is worth keeping
in the file. A socket read is I/O the connection has to do anyway, so the ring
costs nothing extra for it. A timer is not: this is a timerfd_settime syscall
plus an SQE and a CQE per request, roughly 1.5M syscalls a second at this load,
where the tick completes about 13 timers per drain and makes no syscall at all.
Four interleaved repetitions at 64,000 connections:

  ring  1.50M rps  2007% cpu  p99 104.2ms  p99.9 429.6ms
  tick  1.58M rps  1765% cpu  p99  68.4ms  p99.9 373.4ms

14% more CPU for 5% less throughput. The tick's batching is what wins, so the
precision is not worth buying here. IORING_OP_TIMEOUT would remove the syscall
but not the SQE and CQE, so it is worth re-measuring if that ever lands.

IOXIDE_DELAY_MODE=ring selects it. Both paths verified after the default flip.
…hardware

The two mechanisms cost differently and the difference scales with reactor
count, so the 32-reactor result does not settle it.

The ring timer costs a timerfd_settime syscall plus an SQE and a CQE per
request. That is fixed per request whatever the box. The tick costs one
cross-thread post per reactor per tick, so 128,000 posts a second across 32
reactors and 256,000 across the bench box's 64, against the same per-request
cost on the other side.

Locally the tick wins, over four interleaved repetitions at 64,000 connections:

  ring  1.50M rps  2007% cpu  p99 104.2ms  overshoot 0.18ms
  tick  1.58M rps  1765% cpu  p99  68.4ms  overshoot 0.25-0.82ms

Twice the reactors doubles one side of that and not the other, so this defaults
to the ring to get a reading from the hardware the profile is scored on. If the
ordering holds there, this flips back.

IOXIDE_DELAY_MODE=tick selects the other path. Both verified after the flip:
ring answers /delay/15 in 15.17ms and /delay/200 in 200.32ms, tick in 16.17ms.
Measured on the bench hardware at 16,000 connections and 5ms:

  tick  1,842,424 rps  3435% cpu
  ring  2,462,698 rps  5831% cpu   avg 6.48ms  p99 9.91ms  p99.9 16.50ms

34% more throughput than the tick and 20% more than tokio's 2,054,680, with
1.48ms of overhead on a 5ms wait.

The earlier reading that said the opposite was taken on a 32-reactor box sitting
at 0.8% idle with the load generator on it. The ring costs a timerfd_settime
syscall plus an SQE and a CQE per request, and on a machine with no CPU to spare
that can only come out of throughput, so it lost there. The bench box has 6400%
available and the tick was using 3435%, so the same trade buys something instead
of costing something. Nothing about the mechanism changed between those two
measurements, only whether there was CPU free to spend on it.

That also explains the shape of the tick's result rather than just its size.
The tick completes a timer only when a drain runs, so work arrives in batches
and the reactors idle between them - which is why CPU sat at half the box while
throughput flattened. Handing each deadline to the kernel keeps it flowing.

The trade needs spare CPU to be worth making, so the comment records both
readings and says to re-read it if the profile or the hardware changes.
IOXIDE_DELAY_MODE=tick still selects the other path.
The ring completion was treated as proof the wait was over. It is not, and
under load it demonstrably is not: instrumenting the completion against its own
deadline showed about 0.1% of waits resuming early, the worst of them 14.9ms
into a 15ms delay, which is the entire wait skipped.

The cause is not the one the code was written to expect. The fd is non-blocking
so a read the kernel does not arm a poll for returns -EAGAIN, but that never
appeared in the measurement: every early completion carried a valid 8-byte
expiration count. The fd is reused for every request on a connection, so a read
can pick up an expiration left behind by an earlier one and report a timer that
really did fire, only not this request's.

So the deadline decides rather than the completion. An early one goes back on
the ring instead of resuming, which costs one comparison on a path that is
already making a syscall. Measured: earlyCompletions equalled resubmitted on
every sample, and completions still tracked armings, so it converges rather
than spinning.

The effect on the published 2,462,698 is negligible at 0.1%, but a request that
answers before it waited is wrong regardless of what it does to the number, and
a sampled one would fail validation outright.

120 sequential delays on one connection: none early, none wrong-bodied,
overshoot 0.05-0.46ms.
Experiment. At 64,000 connections the profile has been reporting a gap between
entries that does not survive inspection: instrumenting the wait shows the timer
completing 3.2ms late against 23ms of end-to-end overhead, so the delay is about
13% of what is being measured and the rest is the read and write path. Fewer
connections and a shorter wait puts the load somewhere different, and whether
the ordering between entries holds is the thing worth knowing.

The ceiling is connections over delay, so this moves it from 4.27M to 3.20M. The
best entry so far is 2.24M, which leaves headroom but less than before; the docs
now say so rather than claiming the ceiling does not bind.

Existing results are all at async-64000 and are orphaned by this, so the column
is empty until the profile is re-run. That is accepted for now.

Generator runs clean and badge parity holds at 579 ranks.
The async column drops out until the profile is re-run, since every existing
result is at async-64000.
This is a control rather than a retune. The ceiling is connections over delay,
so 32,000 at 10ms and 16,000 at 5ms both come to 3.20M. Doubling both leaves
connection count as the only thing that changed, which is the question the
profile has been circling.

What is known so far, all on the bench hardware:

  64,000 / 15ms   ceiling 4.27M   ioxide 1,568,519   tokio 2,263,962
  16,000 /  5ms   ceiling 3.20M   ioxide 2,360,000   tokio 2,054,680

The ordering reverses between those two, but they differ in ceiling as well as
in connections, so the comparison does not isolate anything. This one does: a
drop against the 16k/5ms numbers is connection scaling and cannot be the
arithmetic, because the arithmetic is identical.

Docs carry the new figures - a 64-thread blocking server tops out near 6,400
rps here, and 1-2ms of timer overshoot now costs a tenth to a fifth of the wait
rather than a fifth to a third. The best-entry reference moves to 2.36M.

Generator runs clean and badge parity holds at 579 ranks.
Continues the controlled series. Connections and delay double together so the
ceiling does not move, which leaves connection count as the only variable:

  16,000 /  5ms   ioxide 2,360,000 (74% of ceiling)   tokio 2,054,680 (64%)
  32,000 / 10ms   ioxide 2,390,257 (75%)              tokio 2,359,835 (74%)
  64,000 / 20ms   ceiling 3.20M

What the first two points already show is that tokio scales on this axis and
ioxide does not. Doubling the connections took tokio from 2,054,680 to
2,359,835 while ioxide went 2,360,000 to 2,390,257, flat, and it needed 6066%
CPU against tokio's 4079% to stay level - 39,402 requests per CPU percent
against 57,856.

This point matters for a different reason. The original 64,000 result was
1,568,519 against a 4.27M ceiling, 37% of it, and that was read as connections
being the problem. Here the connections are the same and only the arithmetic
differs. If this lands near 2.4M then 64,000 connections were never the
difficulty and the earlier collapse belongs to something else in that
configuration.

Read the numbers with the ceiling in mind: both entries are already at about
75% of 3.20M, and queueing grows quickly that close to the limit, so some of
the convergence between them may be the ceiling rather than the frameworks.

Generator runs clean and badge parity holds at 579 ranks.
The profile was walked along a line where connections and delay double together,
so the ceiling stays 3.20M and connection count is the only thing that moves:

  16,000 /  5ms   ioxide 2,360,000 (74%)   tokio 2,054,680 (64%)
  32,000 / 10ms   ioxide 2,390,257 (75%)   tokio 2,359,835 (74%)
  64,000 / 20ms   ioxide 1,641,196 (51%)   tokio 1,887,483 (59%)

64,000 costs both entries about a third of their throughput against the same
ceiling, and that is the reason to stop at 32,000. Both losing together points
at something they share rather than at either server: the load generator holding
64,000 sockets, and the kernel's own cost for that many. A profile whose
headroom is being spent by its own harness is not measuring what it says it is.

The servers do not argue for stopping here. Measured locally against this build:
one reactor serves 1000 connections at 97% of its own ceiling on a fifth of a
core, and pinning 1000 per reactor while going from 1 to 16 reactors holds
94-97% throughout with latency flat at 20.1ms. Neither connections per reactor
nor reactor count is what bends, so the limit is in the shape of the test.

32,000 is also where the two entries agree, 2,390,257 against 2,359,835, which
is what a profile measuring concurrency rather than harness capacity should look
like. The gap that does remain is in CPU, 6066% against 4079%, and that is a
real difference worth reporting rather than one manufactured by the load.

The docs now record why the connection count is what it is, so the next person
to reach for a bigger number has the measurement instead of the intuition.
…o longer runs

13 rows across 5 entries, none of which the board can show. async-64000 is from
the shape the profile had before it settled at 32,000, and async-32768 and
async-49152 are older still, left behind while the connection count was being
tuned in the first place.

A stale row is not inert here. rebuild_site_data.py keys results by profile and
connection count and never prunes, so anything the CATALOG stops asking for
simply accumulates, and the next person reading these files cannot tell which
numbers the profile actually produces.

  aspnet-minimal, express, fastapi, tokio   async-32768, async-49152, async-64000
  ioxide                                    async-64000

async-db is a different profile and is untouched. The async column is empty
until the profile is re-run for its subscribers.
The delay now keeps deadlines in the reactor's own queue and arms a single
timerfd at the front of it. This is what an event loop normally does - Node and
Bun put the next deadline into the poll they were making anyway - and it is the
only shape that neither syscalls per request nor leaves the reactor asleep past
a deadline that has already passed.

The two it replaces each gave up one of those. The per-request timerfd woke the
reactor exactly on time but cost a timerfd_settime, an SQE and a CQE every
single wait. The tick cost nothing per wait but only completed a timer when
something else happened to wake the reactor, which is what left it idle with
work already due. Arming only when a wait is due before whatever the timer
already holds means most waits cost nothing at all: with waits coming due in
batches of about thirteen, that is one arming per thirteen requests.

Measured on this box, 16,000 connections at 2ms, three interleaved repetitions:

  queue  1.99M rps  1852% cpu  p99 19.93ms  p99.9 34.23ms
  tick   1.98M rps  1861% cpu  p99 18.47ms  p99.9 31.07ms
  ring   1.81M rps  1959% cpu  p99 27.80ms  p99.9 56.03ms

The box is saturated with the load generator on it, so the three compress
together here; the bench box is where ring's per-request syscall started to pay
for itself by keeping reactors fed, and this gets the same wake-up behaviour
without buying it.

IORING_OP_TIMEOUT was tried too, with the deadline handed to the kernel on the
ring. It needs a Reactor.SubmitTimeout that no published ioxide has, and it
measured no better than this: 1.98M and a worse tail, because it is still one
SQE and one CQE per request where this is one per batch. Left out until the
library exposes it.

IOXIDE_DELAY_MODE=ring or =tick still select the other two. Verified on the
default path: 80 sequential delays with none early, mean overshoot 0.18ms, and
32 concurrent distinct delays served in 69.6ms against 1088ms serialised.
Bun.sleep returns a promise the scheduler resolves on a timer, so awaiting it
parks the request and hands the thread back rather than holding it.

Measured on this box: the wait costs bun 22%, 2.09M on the baseline mix against
1.63M on the delay path. That is worth having next to the other JS entry, where
the same wait is free - fulmine goes 1.59M to 1.68M - so the profile has two
runtimes with the same concurrency model and visibly different timer costs.

Verified before subscribing: /delay/10 answers in 10.16ms and /delay/50 in
50.37ms, and 32 overlapping requests carrying delays from 3ms to 65ms complete
in 69.6ms against the 1088ms they would take one at a time, none of them early
and every body its own value.
… the timer

The per-reactor timer was the only thing completing a wait, and deadlines do not
arrive in groups: with one wait per request they are spread continuously, so
each got its own arming and its own read. That is a syscall per request, which
is precisely what holding one timer per reactor was supposed to avoid, and the
published run shows it - 2,179,118 rps at 4251% CPU with 2,150% of the box
sitting idle.

The reactor already passes through the handler loop tens of thousands of times a
second under load. Draining there costs a thread-static read and a peek, so most
waits now finish for nothing at all and the timer is left covering the only case
that needs it: the reactor about to sleep with work already due.

Both drains take the same re-entrancy guard. Completing a wait runs its
continuation inline and that continuation goes straight back round the loop,
which drains again, so without it one connection's resume nests inside another's
for as deep as the queue happens to be.

Locally this is inside the noise, 2.00M at 1877% against 1.99M at 1852%, because
the box is saturated with the load generator on it and has no idle CPU to
recover. The bench box has 2,150% of it.

Accuracy is the best of any mode so far: /delay/10 answers in 10.02ms and
/delay/200 in 200.09ms, 100 sequential waits on one connection are none early
with a mean overshoot of 0.15ms, and 32 concurrent distinct delays complete in
69.8ms against 1088ms serialised.
The profile is closed-loop: every connection holds exactly one request for the
length of its wait, so what comes out is connections divided by latency, and the
generator is part of that latency. It follows that the number is not the
server's alone, and measurement says so - holding the server fixed on a 32-core
box and changing only the generator's threads took the result from 1.42M at 8 to
2.16M at 16, a 52% move with nothing on the server side touched.

Both entries also sit well under the hardware they are given, 4251% and 4068% of
6400%, and they sit there together despite being unrelated implementations. Two
different servers agreeing on how much of the box to leave idle points at
something they share.

ASYNC_THREADS defaults to 128 here, against the 64 the profile has been running,
to find out which side of the curve the bench hardware is on. If throughput does
not move, the generator was not the constraint and the remaining 3.5ms of
queueing above the 10ms wait belongs elsewhere. If it does move, the profile has
been reporting the generator as much as the server and the number needs revising
before anyone reads a ranking off it.
Both were tested on the bench hardware and both were wrong.

The generator was not the constraint. tokio is the control - unchanged code, only
the thread count moved - and it went 2,366,578 to 2,364,326 across 64 to 128
threads. A tenth of a percent. So the ~3.5ms of queueing above the 10ms wait does
not belong to the harness, and the earlier 52% swing on a 32-core box was that
box being oversubscribed rather than anything about the profile.

That control also isolates the other change, and it cost throughput: ioxide went
2,179,118 to 2,089,696, down 4.1%, with the thread count accounted for by tokio
sitting still.

The reason is a hole in the idea rather than a tuning matter. Draining on the hot
path never cancelled the armed timerfd, so the timer still fired and still cost
its arm and its read; every pass of the handler loop simply added a peek on top.
It could only ever be more work. Cancelling the timer to make the drain worth
having would cost a syscall of its own, which is the thing being avoided, so
there is nothing to salvage here.

Back to the per-reactor timer alone, which measured 2,179,118 at 4251% CPU -
31% better throughput per unit CPU than the per-request timerfd it replaced,
and level with tokio on CPU where that was 40% above it.

ASYNC_THREADS stays as a knob, documented and defaulted to THREADS' 64, since
knowing the generator is not the constraint is worth being able to re-check.
Both entries leave a third of the box unused and they leave it together, which
has looked like a cap all along. The arithmetic says otherwise: the profile is
closed-loop, so work is capped at connections over delay, and at the ~19.5us of
CPU a request costs, running the 3.20M ceiling would take about 6240% of the
6400% available. ioxide sits at 2.18M, 68% of the ceiling, using 4251% - which
is 68% of that. On those numbers nothing is blocked; there is simply no more
work to be had.

That is a prediction rather than a proof, and halving the cores tests it. 16
cores with their SMT siblings is 32 logical CPUs and a 3200% budget, which at
19.5us per request buys about 1.64M.

  drops to ~1.6M and pins near 3200%   the server was never blocked, only idle
  holds ~2.18M at ~3200%               per-request cost halved, so the wider
                                       set was losing CPU to contention

The second outcome is the interesting one and would mean the extra cores were
costing more than they returned - cross-reactor traffic, memory bandwidth or
SMT siblings fighting over a core. The first says the remaining throughput is
bought by cutting the latency tail rather than by adding CPU, which is where the
zrk numbers already point: p50 sits at 2.21ms against a 2ms wait while p99 is
152ms, so the mean that divides into the connection count is made almost
entirely of tail.

Experiment on the branch, not a proposal for the profile.
Halving the server's cores answered the question the idle CPU had been posing,
and not the way the arithmetic predicted: tokio pinned its 3200% while ioxide
did not. Two entries on identical hardware and an identical ceiling, one able to
spend everything it is given and one not. So ioxide is blocked rather than out
of work, and the model that said otherwise - work capped at connections over
delay, everyone equally short of it - is wrong about ioxide specifically.

The per-request timerfd is the mode that has ever driven its CPU up: 5988% at
the full cpuset against the per-reactor timer's 4251%, for 2,336,243 against
2,179,118. That looked like waste at the time and was reverted on those grounds,
since it bought 7% more throughput for 41% more CPU. On 16 cores the reading is
different: a mode that can occupy the machine is worth more than a mode that
cannot, and whether it can is exactly what is being asked.

What separates them is where the wake-up comes from. The per-request timer hands
every deadline to its own submission, so each one wakes the reactor on its own
account. The per-reactor timer holds one deadline at a time, so a reactor with
work due behind the front of its queue stays asleep until the front fires. That
is the shape that would show up as a reactor unable to fill its core.

IOXIDE_DELAY_MODE=queue and =tick still select the other two.
… measure it

Sixteen cores separated the two existing modes cleanly, and neither is good
enough:

  tokio          1,769,456   3236% of 3200%   18.3us/req
  ioxide queue   1,335,794   2678% of 3200%   20.0us/req
  ioxide ring    1,211,556   3192% of 3200%   26.3us/req

Per request, the per-reactor timer is nearly level with tokio. It loses on
occupancy: it holds one deadline at a time, so wake-ups arrive in bursts and the
reactor sleeps between them with work already due, leaving 522% of the box
unused. The per-request timerfd has the opposite problem - every deadline wakes
the reactor on its own account, which fills the box, but a timerfd_settime per
wait means the CPU goes into syscalls rather than into serving, and it delivers
less while using more.

Neither can have both, and IORING_OP_TIMEOUT is the shape that can: the deadline
rides in the SQE, submitted with the batch the reactor was sending anyway. Every
wait wakes the reactor on its own account, and nothing is spent to arrange it.

ioxide has submitted IORING_OP_TIMEOUT for its own ticker since long before this,
but never exposed it; Reactor.SubmitTimeout does, in 68 lines that reuse the
existing slot allocation and completion routing. That is committed separately on
the library's own branch as MDA2AV/ioxide#212.

The package is vendored rather than published because this is a measurement, not
a release. localfeed/ holds one 140 KB build and NuGet.config adds it beside
nuget.org, so the runner restores it out of the Docker build context with no feed
access; every other package resolves normally. Both go away with the answer -
either the API ships and this becomes an ordinary version bump, or the mode loses
and all of it comes out.

Verified on the default path: /delay/10 answers in 10.11ms and /delay/200 in
200.20ms, 100 sequential waits on one connection are none early with a mean
overshoot of 0.28ms, and 32 concurrent distinct delays complete in 69.3ms against
1088ms serialised. IOXIDE_DELAY_MODE=ring, =queue and =tick still select the
other three.
…lace

Sixteen cores were a diagnostic and they answered it. Every mode, CPU per
request and how much of the 3200% each could actually occupy:

  tokio          1,751,948   3228%   18.4us
  ioxide native  1,422,729   3205%   22.5us
  ioxide queue   1,335,794   2678%   20.0us
  ioxide ring    1,211,556   3192%   26.3us

The per-reactor timer is the cheapest of the three and still loses, because
cheap per request is worth nothing when 522% of the box goes unused. The
per-request timerfd occupies the machine and loses harder, because what it puts
the CPU to is timerfd_settime. IORING_OP_TIMEOUT is the only one that occupies
the machine and spends it on serving, and it wins ioxide's side by 6.5%.

Restoring the cpuset because the constrained box has now said what it had to
say, and because the mode that was chosen on it has more room to show on the
one the profile actually uses. At 32 cores the per-reactor timer was never CPU
limited - 4251% of 6400% - so its throughput was bounded by the idle rather
than by the hardware, and that is the bound this lifts. At 22.5us a request,
6400% is 2.84M against tokio's 2,366,578, under a ceiling of 3.20M.

That is arithmetic, not a result. The per-request cost may not hold once there
are twice the reactors to keep fed.
All seven references go to 0.7.211, the 140 KB build in localfeed/ and the
NuGet.config that pointed at it are deleted, and the Dockerfile restores the way
it always did. That scaffolding existed to measure IORING_OP_TIMEOUT before the
API shipped, the measurement is done and the API has shipped, so it goes.

ReactorDelay is gone with it. It had grown four mechanisms - a userspace tick
posting drains across threads, a per-reactor timerfd with a priority queue, a
per-request timerfd, and the ring timeout - which existed to find out which one
belonged in the entry. The bench hardware answered that, so the other three are
history rather than code, and what is left is 60 lines against one submission.

What replaces it is the RingTimer from MDA2AV/ioxide#213, kept local for now:
ioxide.timer missed the 0.7.211 release even though the SubmitTimeout it needs
did ship, so the class lives here and becomes a package reference when it is
published. It is the same code either way.

The wait is per connection and built on first use, so a connection that never
waits never makes one - which is every connection on every profile but this one.
One op in flight is all a connection needs, because the handler awaits before it
reads again.

Verified against the released packages: /delay/10 answers in 10.02ms and
/delay/200 in 200.09ms, 120 sequential waits on one connection are none early
with a mean overshoot of 0.05ms, 32 concurrent distinct delays complete in
69.4ms against 1088ms serialised, and baseline is 3.75M.
ioxide.timer 0.7.211 is on nuget now, so the RingTimer that was carried here
while the package was missing from the release becomes a package reference and
the file goes. Same code, one fewer thing to keep in step.

ReactorDelay.cs is deleted rather than shrunk. It was 552 lines at its largest,
holding four mechanisms - a userspace tick posting drains across threads, a
per-reactor timerfd with a priority queue, a per-request timerfd, and the ring
timeout - which were there to find out which belonged in the entry. The bench
hardware answered that, so the answer is a package and the other three are
history. The entry now says what it wants and nothing about how:

    await (httpSession.Timer ??= new RingTimer(reactor)).DelayAsync(ms);

One timer per connection, built on first use, so a connection that never waits
never makes one - which is every connection on every profile except this one.

Verified against the published packages: /delay/10 answers in 10.17ms and
/delay/200 in 200.28ms, 120 sequential waits on one connection are none early
with a mean overshoot of 0.11ms, 32 concurrent distinct delays complete in
68.9ms against 1088ms serialised, /json/1 and a 1 MB upload are unaffected, and
baseline is 3.85M.
@MDA2AV
MDA2AV force-pushed the feat/ioxide-async-delay branch from d01b071 to 172499a Compare August 28, 2026 15:15
@MDA2AV

MDA2AV commented Aug 28, 2026

Copy link
Copy Markdown
Owner Author

/benchmark-multiple -f ioxide,tokio -t async

@github-actions

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Frameworks: 2 | Test: async

ioxide

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 32000 2,504,680 5519.0% 1.4GiB +7.2% +7.7%

tokio

Test Conn RPS CPU Mem Δ RPS Δ Mem
async 32000 2,365,701 4106.7% 1.4GiB NEW NEW

@MDA2AV
MDA2AV merged commit ff12371 into main Aug 28, 2026
5 checks passed
@MDA2AV
MDA2AV deleted the feat/ioxide-async-delay branch August 30, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant