Performance and feature comparison
Every number and every cell on this page is measured, not quoted from documentation: the same benchmark harness drives the same live exchange against each product running unmodified in its own container, and the feature matrix comes from the live interop verification runs. Tested versions are listed; newer releases may differ.
Benchmark
Scenario: the exchange profile every tested product supports — payload signed (RSA / SHA-256) and encrypted (AES-256-CBC), synchronous signed MDN, MIC reconciliation. One uniform sender (the as2d client) drives each receiver; latency is the full exchange as a partner experiences it: packaging, POST, the receiver’s decrypt/verify/store, MDN generation, and receipt verification.
Method: 100 messages per payload size after warmup, 4 concurrent senders, containerized receivers with their default configurations, all on the same host (a 12th-gen Intel i7-12700H laptop, NVMe SSD, Linux). Receivers were benchmarked one at a time. Results from 2026-08-26; raw data and the harness ship in the as2d source tree, so the run is reproducible.
| Receiver | 1 KiB msgs/s | 1 KiB p50 ms | 1 KiB p95 ms | 64 KiB msgs/s | 64 KiB p50 ms | 64 KiB p95 ms | 1 MiB msgs/s | 1 MiB p50 ms | 1 MiB p95 ms |
|---|---|---|---|---|---|---|---|---|---|
| as2d | 306 | 12.7 | 16.6 | 279 | 14.0 | 18.4 | 125 | 31.6 | 35.0 |
| phase2 | 235 | 14.6 | 30.4 | 166 | 23.5 | 29.7 | 27.8 | 143 | 153 |
| openas2 | 308 | 12.8 | 18.9 | 338 | 11.4 | 14.7 | 73.7 | 53.0 | 64.7 |
| mendelson | 37.3 | 101 | 181 | 33.2 | 118 | 190 | 5.55 | 711 | 1109 |
| pyas2 | 99.2 | 36.1 | 71.6 | 67.4 | 56.9 | 88.5 | 16.4 | 239 | 319 |
| waarp-as2 | 670 | 5.77 | 8.30 | 520 | 7.77 | 9.82 | 148 | 26.6 | 31.1 |
| ruby-as2 | 158 | 24.5 | 39.8 | 154 | 25.9 | 33.6 | 85.4 | 46.0 | 58.5 |
| node-libas2 | 71.9 | 55.6 | 63.2 | 53.6 | 73.1 | 85.4 | — | — | — |
Reading notes:
- What “durable” costs. as2d’s numbers include per-message durable state writes — replay-protection and journal entries are fsynced to disk before the receipt is acknowledged, because an acknowledged message must survive a crash. Products differ in what they persist before acknowledging, and this benchmark does not normalize that.
- as2d is nearly size-insensitive: median latency rises only 13 ms → 32 ms from 1 KiB to 1 MiB. It is the fastest receiver measured at every payload size except the Go implementation — while being the only product in the table whose numbers include fsyncing the payload, replay-protection state, and the journal before acknowledging (at 1 MiB it is still 4× phase2 and 22× mendelson CE).
- The crypto runtime matters as much as the product. as2d’s own
container numbers doubled just by moving its base image from OpenSSL
3.0 to 3.5 (3.0’s per-operation algorithm-fetch locking is a known
cost under concurrent CMS load that microbenchmarks like
openssl speeddo not show). If you run any AS2 product on an OpenSSL-3.0-era distribution, you are likely paying this too. - Waarp AS2 (Go) is the fastest receiver at small payloads; at 1 MiB it and as2d are effectively tied at the top.
- node-libas2 failed every 1 MiB exchange under load (it returned MDNs with no disposition field), hence the dashes — consistent with the library’s frozen 2020-era dependency set.
- Java products show their strength at small payloads once the JIT is warm (OpenAS2, phase2) but fall off steeply at 1 MiB.
Scaling under load
The same exchange at 16 concurrent senders: as2d’s durable writes batch across in-flight messages (group commit), so throughput scales near-linearly — 981 msg/s at 1 KiB, 800 msg/s at 64 KiB, and at 1 MiB it matches the fastest non-durable implementation measured (335 vs 342 msg/s) while fsyncing every payload before acknowledging it.
Feature matrix
As measured in the live interop rig (both directions, receipt reconciliation) — ✔ works as specified, ◐ works with caveats (see the per-product notes), ✘ not supported by that product.
| Capability | as2d | phase2 (as2-lib) | OpenAS2 4.10 | mendelson CE 1.1b69 | pyas2lib 1.4.4 | Waarp AS2 | Ruby as2 0.12 | node-libas2 0.8.2 |
|---|---|---|---|---|---|---|---|---|
| Signing SHA-2 family | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
| RSASSA-PSS signatures | ✔ | ✔ | ✔ | — | ✘ | — | — | — |
| AES-CBC / 3DES encryption | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
| AES-GCM (RFC 5084 conformant) | ✔ | ✘ rejects | — | — | — | ✘ rejects | — | — |
| AES-GCM (BC-compatible form) | ✔ receive + per-partner send | ✔ | — | ✔ (its native form) | — | ✘ rejects | — | — |
| ZLIB compression (RFC 5402) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ | ✘ |
| Sync signed MDN generation | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ◐ broken upstream (sends unsigned) |
| Async MDN delivery | ✔ | ✔ | ✔ | — | — | — | ✘ | ✘ |
| Honors requested receipt digest | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ always SHA-256 | ✘ uses signature digest |
| Binary (raw) S/MIME bodies | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ base64 required |
| Enforces inbound security floor | ✔ (insufficient-message-security) | — | — | ✔ | — | — | ✔ (requires signed+encrypted, always) | — |
✘ is only used where the limitation was directly measured or is the product’s own documented constraint; — means the capability was not exercised against that product in the rig and is left unclaimed rather than assumed either way.
A note on fairness. These are open-source products measured under their own default configurations in containers; each of them may be tunable beyond what defaults deliver, and all of them interoperate correctly with as2d in the profiles above — that is the point of the interop rig. The benchmark answers one question honestly: what does the same exchange cost against each receiver, out of the box, on the same hardware.