Technology Aug 26, 2026 · 3 min read

Writing QUIC in Pure Java

I maintain gumdrop, an async, non-blocking Java server framework. Last year I wanted to add HTTP/3 support, and ran into a wall: the Java ecosystem essentially doesn't have QUIC. The JDK's own experimental support (JEP 517) is client-only. Netty gets HTTP/3 by shelling out to quiche + BoringSSL over...

DE
DEV Community
by Chris Burdess
Writing QUIC in Pure Java

I maintain gumdrop, an async, non-blocking Java server framework. Last year I wanted to add HTTP/3 support, and ran into a wall: the Java ecosystem essentially doesn't have QUIC. The JDK's own experimental support (JEP 517) is client-only. Netty gets HTTP/3 by shelling out to quiche + BoringSSL over JNI — which works, but you're back to native builds, platform-specific binaries, and a C library sitting underneath your "pure Java" framework. I used that approach first. It was clumsy enough that I went looking for a pure-Java alternative.

There's exactly one: Kwik. But Kwik is blocking per connection — one thread per QUIC connection.

That's a non-starter for a framework built around single-threaded selector loops handling tens of thousands of concurrent connections. So I wrote a QUIC implementation from scratch: packet protection, loss detection and NewReno congestion control, connection migration, 0-RTT, QPACK, an HTTP/3 client and server — all driven by the same non-blocking event loop as everything else in gumdrop.

Collaboration note: TLS 1.3 comes from Agent15 — also from Kwik's author, Peter Doornbosch, but just the handshake layer, not the connection model. We're currently working together on making PQC — hybrid key exchange and signatures — the default there.

Why the thread model matters

The reason this mattered beyond HTTP/3: gumdrop isn't a web framework with QUIC bolted on, it's a general async I/O framework, and QUIC is just a transport. One thread per connection is exactly the model gumdrop exists to avoid — it caps concurrency at your thread pool, not your file descriptors, and it's the reason a "just use Kwik" fix was never really on the table.

The same QUIC stack backs DNS-over-QUIC (DoQ) as a first-class DNS transport alongside DoT, DoH, UDP, and TCP — and the DNS resolver itself is fully async, with no blocking InetAddress.getByName() anywhere in the I/O path, which is its own small miracle in Java. HTTP, SMTP, IMAP, POP3, FTP, MQTT, SOCKS — it's the same story throughout: single-threaded selector loops, a worker pool sized independently of connection count, ByteBuffer end to end, no thread-per-connection anywhere in the framework.

Aside — the servlet container: There's also a servlet container that runs transparently on top of all this, including HTTP/3 — which is a strange thing to want, since the Servlet API's I/O model is inherently synchronous even with its async extensions. But it means existing J2EE code gets HTTP/3 for free without a rewrite, which is a decent trick even if the API underneath it is fighting the rest of the framework's philosophy.

Where it stands

The QUIC layer ships today: packet protection, NewReno, QPACK dynamic tables, 0-RTT, Retry packets, passive connection migration, WebSocket-over-HTTP/3 (RFC 9220), and automatic h3/h2/h1.x negotiation. No native build step, no QUICHE_DIR, no BoringSSL — just Java 17+.

Code, protocol notes, and the rest of the framework it grew out of: github.com/cpkb-bluezoo/gumdrop

DE
Source

This article was originally published by DEV Community and written by Chris Burdess.

Read original article on DEV Community
Back to Discover

Reading List