Skip to content

Add Helidon SE 4.4 (Java — Níma virtual threads web server)#235

Open
BennyFranciscus wants to merge 2 commits intoMDA2AV:mainfrom
BennyFranciscus:add-helidon
Open

Add Helidon SE 4.4 (Java — Níma virtual threads web server)#235
BennyFranciscus wants to merge 2 commits intoMDA2AV:mainfrom
BennyFranciscus:add-helidon

Conversation

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

Helidon SE 4.4.0

Adds Helidon SE — Oracle's cloud-native Java framework with its own Níma WebServer built entirely on Java 21 virtual threads.

Why Helidon?

HttpArena already has several JVM frameworks but they all use either Netty or Tomcat as their underlying server:

  • Spring Boot → Tomcat
  • Quarkus → Vert.x/Netty
  • Vert.x → Netty
  • Ktor → Netty

Helidon SE 4 is the only JVM entry with its own custom web server (Níma) that uses a thread-per-request model on virtual threads instead of event loops. This makes it a genuinely different architecture to benchmark against the reactive/event-loop entries.

Implementation

  • Framework: Helidon SE 4.4.0 (WebServer)
  • Engine: Níma (virtual threads, no Netty)
  • JVM: Eclipse Temurin 25, ZGC
  • JSON: Jackson (consistent with other JVM entries)
  • SQLite: JDBC with thread-local connections, read-only + mmap
  • PostgreSQL: HikariCP connection pool (64 max, 16 idle)
  • Compression: Manual gzip with BEST_SPEED level

Endpoints

Test Endpoint Method
baseline /baseline11 GET, POST
baseline-h2 /baseline2 GET
pipelined /pipeline GET
json /json GET
upload /upload POST
compression /compression GET
db /db GET
async-db /async-db GET
static /static/{filename} GET

Validation

  • Docker build: ✅ Clean build with multi-stage (maven → temurin JRE)
  • Server startup: ✅ ~18ms (Helidon 4.4.0 with virtual threads)
  • All endpoints tested and returning correct responses

Notes

Helidon 4 rewrote the SE API from async/reactive to blocking (enabled by virtual threads). The result is simpler code that still gets high throughput — will be interesting to see how it stacks up against the Netty-based frameworks.

cc @tomas-langer @spericas (Helidon maintainers)

Helidon SE 4.4.0 — Oracle's cloud-native Java framework with its own
Níma WebServer built on Java 21 virtual threads. Unlike other JVM entries
(Spring/Tomcat, Quarkus/Netty, Vert.x/Netty, Ktor/Netty), Helidon uses
a custom server with thread-per-request on virtual threads.

Endpoints: baseline, pipelined, limited-conn, json, upload, compression,
noisy, mixed, async-db (HikariCP + PostgreSQL), static files.

Uses Jackson for JSON, SQLite JDBC (thread-local, read-only, mmap),
HikariCP for PostgreSQL connection pooling.

JVM: Eclipse Temurin 25, ZGC, UseNUMA, AlwaysPreTouch.
Copy link
Copy Markdown

@jerrythetruckdriver jerrythetruckdriver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-XX:-StackTraceInThrowable disables stack trace generation on all exceptions. No production deployment guide recommends this — it trades debuggability for speed and falls under "experimental or unstable options that trade safety for speed" (implementation rules).

Drop that flag. The other JVM flags (ZGC, UseNUMA, AlwaysPreTouch) are standard production tuning and fine to keep.

Drops the flag per review feedback — disabling stack traces
trades debuggability for speed, which violates implementation
rules on experimental/unsafe options.
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Good call — dropped -XX:-StackTraceInThrowable. The other flags (ZGC, UseNUMA, AlwaysPreTouch) are standard production tuning so those stay.

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 29, 2026

/benchmark

@github-actions
Copy link
Copy Markdown

🚀 Benchmark run triggered for helidon (all profiles). Results will be posted here when done.

@github-actions
Copy link
Copy Markdown

Benchmark Results

Framework: helidon | Profile: all profiles

helidon / baseline / 512c (p=1, r=0, cpu=64)
  Best: 18749 req/s (CPU: 308.9%, Mem: 8.7GiB) ===

helidon / baseline / 4096c (p=1, r=0, cpu=64)
  Best: 18271 req/s (CPU: 309.5%, Mem: 9.0GiB) ===

helidon / baseline / 16384c (p=1, r=0, cpu=64)
  Best: 14824 req/s (CPU: 302.7%, Mem: 11.0GiB) ===

helidon / pipelined / 512c (p=16, r=0, cpu=unlimited)
  Best: 8565318 req/s (CPU: 7240.7%, Mem: 16.2GiB) ===

helidon / pipelined / 4096c (p=16, r=0, cpu=unlimited)
  Best: 6030523 req/s (CPU: 5478.2%, Mem: 21.3GiB) ===

helidon / pipelined / 16384c (p=16, r=0, cpu=unlimited)
  Best: 5465407 req/s (CPU: 5888.0%, Mem: 20.7GiB) ===

helidon / limited-conn / 512c (p=1, r=10, cpu=unlimited)
  Best: 17827 req/s (CPU: 358.7%, Mem: 8.7GiB) ===

helidon / limited-conn / 4096c (p=1, r=10, cpu=unlimited)
  Best: 16817 req/s (CPU: 353.5%, Mem: 9.4GiB) ===

helidon / json / 4096c (p=1, r=0, cpu=unlimited)
  Best: 67454 req/s (CPU: 2416.8%, Mem: 17.2GiB) ===

helidon / json / 16384c (p=1, r=0, cpu=unlimited)
  Best: 221482 req/s (CPU: 6572.5%, Mem: 22.0GiB) ===

helidon / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 743 req/s (CPU: 4696.0%, Mem: 18.6GiB) ===

helidon / upload / 256c (p=1, r=0, cpu=unlimited)
  Best: 787 req/s (CPU: 5456.0%, Mem: 13.0GiB) ===

helidon / upload / 512c (p=1, r=0, cpu=unlimited)
  Best: 722 req/s (CPU: 4494.6%, Mem: 14.2GiB) ===

helidon / compression / 4096c (p=1, r=0, cpu=unlimited)
  Best: 7419 req/s (CPU: 9523.0%, Mem: 16.9GiB) ===

helidon / compression / 16384c (p=1, r=0, cpu=unlimited)
  Best: 8413 req/s (CPU: 12138.2%, Mem: 10.5GiB) ===

helidon / noisy / 512c (p=1, r=0, cpu=unlimited)
  Best: 10358 req/s (CPU: 5004.5%, Mem: 13.8GiB) ===

helidon / noisy / 4096c (p=1, r=0, cpu=unlimited)
  Best: 4886 req/s (CPU: 6044.2%, Mem: 12.6GiB) ===

helidon / noisy / 16384c (p=1, r=0, cpu=unlimited)
  Best: 3695 req/s (CPU: 6576.1%, Mem: 15.1GiB) ===

helidon / mixed / 4096c (p=1, r=5, cpu=unlimited)
  Best: 17899 req/s (CPU: 4363.4%, Mem: 26.9GiB) ===

helidon / mixed / 16384c (p=1, r=5, cpu=unlimited)
  Best: 16643 req/s (CPU: 5542.0%, Mem: 24.9GiB) ===

helidon / static / 4096c (p=1, r=0, cpu=unlimited)
  Best: 1011280 req/s (CPU: 4965.4%, Mem: 12.7GiB) ===

helidon / static / 16384c (p=1, r=0, cpu=unlimited)
  Best: 604259 req/s (CPU: 5414.0%, Mem: 17.1GiB) ===

helidon / async-db / 512c (p=1, r=0, cpu=unlimited)
  Best: 9620 req/s (CPU: 624.9%, Mem: 8.6GiB) ===

helidon / async-db / 1024c (p=1, r=0, cpu=unlimited)
  Best: 11997 req/s (CPU: 783.1%, Mem: 18.6GiB) ===
Full log
    Latency   18.30ms   8.51ms   47.00ms   166.50ms   901.80ms

  3005537 requests in 5.01s, 2989153 responses
  Throughput: 597.16K req/s
  Bandwidth:  6.67GB/s
  Status codes: 2xx=2989153, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2989153 / 2989153 responses (100.0%)
  Per-template: 217201,218172,219349,69740,70941,220822,210724,69388,68838,72705,219743,225167,68849,67859,218024,228549,167800,67166,67312,220804
  Per-template-ok: 217201,218172,219349,69740,70941,220822,210724,69388,68838,72705,219743,225167,68849,67859,218024,228549,167800,67166,67312,220804
  CPU: 5197.6% | Mem: 12.4GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 20
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   18.48ms   9.53ms   28.50ms   200.20ms    1.19s

  3134107 requests in 5.16s, 3117979 responses
  Throughput: 604.29K req/s
  Bandwidth:  6.80GB/s
  Status codes: 2xx=3117979, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 3117979 / 3117979 responses (100.0%)
  Per-template: 228737,218795,233056,87334,84467,211815,220210,83992,80110,84757,234426,243720,83357,80523,198856,183557,158323,83658,85328,232958
  Per-template-ok: 228737,218795,233056,87334,84467,211815,220210,83992,80110,84757,234426,243720,83357,80523,198856,183557,158323,83658,85328,232958
  CPU: 5414.0% | Mem: 17.1GiB

=== Best: 604259 req/s (CPU: 5414.0%, Mem: 17.1GiB) ===
  Input BW: 32.85MB/s (avg template: 57 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-helidon
httparena-bench-helidon

==============================================
=== helidon / async-db / 512c (p=1, r=0, cpu=unlimited) ===
==============================================
2d7f2e601f4d8319066e4e1eabf66bd86f68463b50eb80c25bc711aef0ffe1b1
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   59.45ms   42.90ms   91.90ms   184.70ms   258.90ms

  43419 requests in 5.00s, 43419 responses
  Throughput: 8.68K req/s
  Bandwidth:  69.52MB/s
  Status codes: 2xx=43419, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 43419 / 43419 responses (100.0%)
  CPU: 1995.2% | Mem: 5.8GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   53.23ms   42.90ms   82.00ms   92.90ms   125.90ms

  48101 requests in 5.00s, 48101 responses
  Throughput: 9.62K req/s
  Bandwidth:  75.77MB/s
  Status codes: 2xx=48101, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 48101 / 48101 responses (100.0%)
  CPU: 624.9% | Mem: 8.6GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   77.35ms   79.90ms   87.90ms   109.90ms   119.60ms

  33069 requests in 5.00s, 33069 responses
  Throughput: 6.61K req/s
  Bandwidth:  52.68MB/s
  Status codes: 2xx=33069, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 33069 / 33069 responses (100.0%)
  CPU: 593.8% | Mem: 18.7GiB

=== Best: 9620 req/s (CPU: 624.9%, Mem: 8.6GiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-helidon
httparena-bench-helidon

==============================================
=== helidon / async-db / 1024c (p=1, r=0, cpu=unlimited) ===
==============================================
7ae0a8c2758519eb0454e405e97ec82c3a2cfa6d249e05431fc2a8143b10475b
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   97.42ms   62.90ms   177.00ms   254.30ms   288.20ms

  52295 requests in 5.00s, 52295 responses
  Throughput: 10.45K req/s
  Bandwidth:  83.14MB/s
  Status codes: 2xx=52295, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 52295 / 52295 responses (100.0%)
  CPU: 1303.1% | Mem: 7.0GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   97.05ms   59.00ms   165.90ms   204.80ms   221.20ms

  52312 requests in 5.00s, 52312 responses
  Throughput: 10.46K req/s
  Bandwidth:  83.17MB/s
  Status codes: 2xx=52312, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 52312 / 52312 responses (100.0%)
  CPU: 831.3% | Mem: 11.1GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   85.36ms   59.00ms   166.00ms   198.90ms   212.50ms

  59986 requests in 5.00s, 59986 responses
  Throughput: 11.99K req/s
  Bandwidth:  96.06MB/s
  Status codes: 2xx=59986, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 59986 / 59986 responses (100.0%)
  CPU: 783.1% | Mem: 18.6GiB

=== Best: 11997 req/s (CPU: 783.1%, Mem: 18.6GiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-helidon
httparena-bench-helidon
[skip] helidon does not subscribe to baseline-h2
[skip] helidon does not subscribe to static-h2
[skip] helidon does not subscribe to baseline-h3
[skip] helidon does not subscribe to static-h3
[skip] helidon does not subscribe to unary-grpc
[skip] helidon does not subscribe to unary-grpc-tls
[skip] helidon does not subscribe to echo-ws
httparena-postgres
httparena-postgres
[restore] Restoring CPU governor to performance...

@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Benchmark results look solid! 🎉

Highlights:

  • Pipelined: 8.5M req/s at 512c — virtual threads absolutely shine here
  • Static: 1M req/s at 4096c — strong
  • JSON: scales from 67K (4096c) to 221K (16384c) — impressive scaling
  • Mixed: 17.9K req/s — consistent across concurrency levels
  • Baseline: 18.7K req/s under the 64-CPU cap — efficient

Upload at ~750 req/s is on the lower end but not unusual for JVM frameworks handling 20MB bodies. Memory peaks at 18.6GiB during upload which is reasonable.

Async-db at 9.6-12K req/s with 625-783% CPU shows Helidon's virtual threads keeping DB connections busy without over-scheduling.

No 5xx errors across any profile, which is great stability-wise. Looks ready to ship.

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.

3 participants