Skip to content

CE Performance Benchmark — 2026-05-25

Host: local dev machine (4 cores, 15 GiB RAM, Debian Linux)
Node: height 10,064 — chain file 1.7 MB (bincode + zstd-3)
Binary: release build from main @ dcb2a9f
Config: ce start --port 4001 --api-port 8080 --bootstrap/relay 178.105.145.170:4001


Setup

cargo build --release            # 3 min 16 s (cold, 4 cores)
cp target/release/ce ~/.local/bin/ce
ce start --port 4001 --api-port 8080 \
  --bootstrap /ip4/178.105.145.170/tcp/4001/p2p/<relay-id> \
  --relay     /ip4/178.105.145.170/tcp/4001/p2p/<relay-id>

ce is installed at ~/.local/bin/ce (already in PATH).


Results

1. HTTP API Latency (100 sequential requests, loopback)

Endpointp50p95p99max
GET /health0.32 ms0.43 ms10.92 ms10.92 ms
GET /status0.32 ms0.39 ms0.49 ms0.49 ms
GET /jobs0.32 ms0.38 ms0.46 ms0.46 ms
GET /signals0.32 ms0.46 ms2.28 ms2.28 ms
GET /atlas0.32 ms0.46 ms0.57 ms0.57 ms
GET /bootstrap0.34 ms0.40 ms0.49 ms0.49 ms

Axum on Tokio is fast — median 0.32 ms, well within budget for all read paths.

The occasional p99 spike on /health (10.92 ms) is OS scheduling jitter, not application code.

2. Mining Rate

MetricValue
Blocks in 30 s3
Rate0.100 blocks/s
Interval10.0 s/block

Exactly on the 10-second mining ticker target. PoW difficulty is 0 (no leading-zero requirement) so every hash seals instantly — block rate is entirely timer-driven.

3. CLI Cold-Read Performance (ce status)

MetricValue
p5030.9 ms
p9532.5 ms
min30.2 ms
max32.5 ms
Chain height10,041
Bytes/block~170 B compressed (~1,358 B uncompressed est.)
Deserialize rate325,055 blocks/s

ce status reads and fully deserializes the chain from disk on every invocation. At 10K blocks this costs ~30 ms. The decompress + deserialize path is fast — most of the time is process startup + zstd decompression of the 1.7 MB file.

4. API Throughput (concurrent requests)

ConcurrencyGET /status RPSGET /signals RPS
11,0182,056
24,0913,431
44,1283,735
82,9743,497
164,5563,690
324,8354,479
646,5965,236

Throughput is good and scales with concurrency. The variance between runs reflects Python threading overhead in the test harness rather than Axum behavior.

5. Lock Contention Under Load

Sequential vs. concurrent (16 reader threads):

PathSequential p50Concurrent p50
GET /health0.32 ms3.59 ms
GET /status0.32 ms3.59 ms

Under 16 concurrent readers, latency grows from 0.32 ms to 3.59 ms — a 11x increase — because both /status and /signals acquire an exclusive tokio::sync::Mutex, serializing all concurrent reads.

6. Signal Submission

Pathp50p95p99max
POST /signals/send0.37 ms0.69 ms11.75 ms

Fast for single-threaded use; subject to same mutex contention under load.

7. Process Resources (after 4 min uptime)

MetricValue
RSS70.8 MB
VSZ388 MB
CPU0.6%
MEM0.4%

Very lean. The node is mostly idle between 10-second mining ticks.


Performance Gain Areas

P1 — Replace Mutex<Chain> with RwLock<Chain>

Impact: high
Every read endpoint (/status, /signals, /jobs, /atlas, /bootstrap) acquires an exclusive tokio::sync::Mutex<Chain> lock. Only write operations (block append, tx pool update) need exclusivity.

Switching to tokio::sync::RwLock lets read requests run concurrently with zero contention, eliminating the 11x latency spike at c=16 (3.59 ms → ~0.32 ms).

Affected file: crates/ce-node/src/lib.rs:169chain: Arc<Mutex<Chain>> and all usage sites.

The same applies to SignalRing and Atlas — both are read-heavy.

P2 — Status sidecar file for ce status CLI

Impact: medium
ce status fully deserializes the chain every time (30 ms at 10K blocks, ~300 ms at 100K). The CLI only needs height, difficulty, and balance — all maintained in Chain's O(1) caches.

Write a small ~/.local/share/ce/status.json (height, balance, difficulty, timestamp) on every block seal. ce status reads this 200-byte file in <1 ms instead of deserializing the full chain.

Relevant save path: crates/ce-node/src/lib.rs:257chain_path2 in the mining loop.

P3 — Peer observability: INFO log + /peers endpoint

Impact: medium (ops/debugging)
The mesh swarm has no INFO-level log for connection established/closed. In production, there is no way to verify peer connectivity without enabling RUST_LOG=ce_mesh=debug. This makes diagnosing relay/NAT issues extremely hard.

Two changes needed:

  1. Add info!("connected to peer {peer_id}") in ce-mesh::handle_event on ConnectionEstablished.
  2. Add a GET /peers API endpoint that returns connected peer IDs and their announce heights.

Relevant file: crates/ce-mesh/src/lib.rs:566handle_event.

P4 — gVisor availability for container isolation

Impact: medium (security)
Every startup logs WARN: gVisor not available, falling back to runc. Without gVisor, container isolation relies on runc's Linux namespace stack instead of the gVisor VM boundary. This is the only persistent WARN on startup and directly weakens the adversarial compute isolation model.

Install gVisor (runsc) and configure Docker/containerd to use it as the ce runtime:

bash
sudo apt-get install runsc
sudo runsc install

Relevant file: crates/ce-container/src/lib.rs (detection logic).

P5 — Gossip deduplication / message size

Impact: low-medium, mesh-scale dependent
At current chain height (10K) the block gossip payload includes the full block header + all transactions encoded with bincode. The chain uses blocks: Vec<Block> — sync responses send up to 500 blocks per response. At ~1,358 bytes/block uncompressed, a 500-block sync response is ~680 KB uncompressed. Adding zstd to gossip wire encoding (already used on disk) would cut mesh bandwidth.

Currently gossip messages are bincode-only. Blocks could be gossiped as bincode+zstd to reduce relay bandwidth.

P6 — Chain rebuild_caches on reorg

Impact: low, correctness path
try_reorg calls rebuild_caches() twice (once for the reorg candidate, once for the winner) — full O(n) scan both times. At scale (100K+ blocks), reorgs will be expensive. The incremental apply_block_to_cache already exists; a targeted undo + re-apply would be O(fork-depth) instead.


Mesh Connectivity Note

The relay at 178.105.145.170:4001 is reachable (TCP confirmed). The libp2p swarm binds port 4001 on all interfaces and dials the relay. No INFO-level connection event is emitted when the relay handshake succeeds (see P3 above), so connectivity can only be verified by watching block height progression against known network state.


Quick Wins Summary

PriorityChangeEffortLatency impact
P1Mutex<Chain>RwLock<Chain>~1 day11x under load
P2Status sidecar file~2 h30 ms → <1 ms (CLI)
P3Peer INFO logs + /peers endpoint~2 hOps quality
P4Install gVisor~30 minSecurity
P5Gossip zstd compression~4 hBandwidth
P6Incremental reorg undo~1 dayReorg path only

Served by ce-serve from a content-addressed bundle on the mesh.