rs-netty
Tokio-native typed TCP/UDP pipelines inspired by Netty.
rs-netty keeps the familiar Channel / Pipeline / Handler model, but rebuilds
it around Rust ownership, async/await, Tokio tasks, bounded queues, and typed
messages. The result is a small network framework where invalid pipeline order,
message mismatches, and TCP/UDP pipeline mixups are caught at compile time.
Benchmark Snapshot
These charts come from the benchmark harness in benchmarks/, comparing
rs-netty, bare Tokio, and Java Netty on one local non-loopback interface. They
are useful as a directional snapshot, not a universal performance claim.
Throughput

P99 Latency

Server Memory

Latency Percentiles

Why rs-netty?
- Netty-shaped, Rust-native: keep codec, pipeline, handler, channel, and
lifecycle concepts without Java futures, promises, object messages, or
reference-counted
ByteBuf. - Typed pipeline construction:
codec -> inbound* -> business* -> handler -> outbound*is encoded in the builder API. Invalid stage order simply does not type-check. - TCP and UDP support: stream pipelines for TCP, datagram pipelines for UDP, with separate builder types so they cannot be accidentally mixed.
- No dynamic handler dispatch on the main path: the default pipeline is built
from generic static stages rather than
Box<dyn Handler>. - Practical batteries included: built-in line, length-field, delimiter, fixed-length, byte-array, MQTT, UTF-8 datagram, bytes datagram, and two optional JSON pipeline stages.
- Operational hooks when you need them: optional lifecycle hooks, idle timeout, graceful shutdown handles, bounded outbound queues, and opt-in TCP connection stats.
Quick Start
Add the crate:
[]
= "0.2"
= { = "1", = ["rt-multi-thread", "macros"] }
Build a TCP echo server:
use ;
async
;
async
Talk to it with a typed TCP client:
use ;
async
;
async
Typed Pipelines
TCP uses a stream pipeline:
pipeline()
.codec(...)
.inbound(...)*
.business(...)*
.handler(...)
.outbound(...)*
UDP uses a datagram pipeline:
datagram_pipeline()
.codec(...)
.inbound(...)*
.business(...)*
.handler(...)
.outbound(...)*
Methods only exist in valid states. Message transitions are checked with trait
bounds, so handler inputs must match previous stage outputs, outbound inputs
must match Handler::Write or DatagramHandler::Write, and final outbound
types must be encodable by the selected codec.
TcpServer and TcpClient only accept stream pipelines. UdpServer and
UdpClient only accept datagram pipelines.
UDP Example
use ;
async
;
async
UDP support is datagram-oriented. UdpServer uses one socket-level pipeline and
does not create per-peer child pipelines. If you need per-peer state, store it
explicitly inside your handler, for example with HashMap<SocketAddr, PeerState>.
DatagramContext::write(msg) replies to the current datagram peer.
DatagramContext::write_to(peer, msg) and DatagramChannel::write_to(peer, msg)
send to an explicit peer.
Lifecycle and Operations
Servers and clients can attach optional lifecycle hooks with .life(...). The
default is NoLife, so applications that do not need hooks pay no dynamic
dispatch cost.
use SocketAddr;
use ;
;
bind
.pipeline
.life
.run
.await
Servers also support an external shutdown handle:
let server = bind
.pipeline
.start
.await?;
server.shutdown;
server.wait.await?;
TCP servers and clients can enable an optional idle timeout:
bind
.idle_timeout
.pipeline
.run
.await
When no idle timeout is configured, the TCP connection loop uses the no-timeout path and does not create a timer.
TCP connection stats are opt-in:
bind
.track_connection_stats
.pipeline
.run
.await
When enabled, Context::stats() and Channel::stats() expose connection time,
bytes read/written, and frames read/written. Channels also expose is_closed(),
capacity(), and max_capacity() from the underlying Tokio queue.
Built-In Codecs
Stream codecs:
LineCodecLengthFieldBasedFrameDecoderLengthFieldPrependerFixedLengthFrameDecoderDelimiterBasedFrameDecoderByteArrayDecoderByteArrayEncoderMqttCodec
Optional pipeline stages behind the json feature:
JsonDecode<T>JsonEncode<T>
Datagram codecs:
Utf8DatagramCodecBytesDatagramCodec
JsonDecode<T> and JsonEncode<T> are deliberately small codec-layer helpers:
they parse typed messages into the pipeline and serialize typed responses back
out. rs-netty does not expose a broader JSON API.
Enable these stages with:
[]
= { = "0.2", = ["json"] }
= { = "1", = ["derive"] }
Then keep framing and JSON parsing as separate pipeline stages:
use ;
;
async
let pipeline = pipeline
.codec
.inbound
.handler
.outbound;
derive is only the example path here. Your message types simply need to
implement serde::Deserialize for JsonDecode<T> and serde::Serialize for
JsonEncode<T>.
Benchmarks
The repository includes benchmark harnesses for rs-netty, bare Tokio, and
Java Netty under benchmarks/. They measure throughput, p50/p90/p99/p999
latency, and server RSS across TCP line echo, TCP length-field echo, and UDP
echo scenarios.
Benchmark results depend heavily on host, network path, JVM warmup, payload shape, and protocol behavior. Treat the included chart as a snapshot from one local run, not a universal performance claim.
Examples
Non-Goals
Non-goals for v0.2:
- No EventLoop API.
- No ByteBuf refCnt API.
- No ChannelFuture / Promise API.
- No dynamic
Box<dyn Handler>main path. - No TLS yet.
- No codec registry yet.
- No automatic UDP reliability / ordering / retransmission.
- No per-peer UDP child pipeline yet.