Architecture
Nacelle is organized as a small core plus protocol-specific transport crates.
Crate Layout
nacelle-core: shared handler, request/response body, limits, lifecycle, telemetry, and TLS primitives.nacelle-tcp: TCP/Unix socket server, protocol trait, connection loop, and listener runtime.nacelle-http: Hyper HTTP/1 server, HTTP request policy, and HTTP TLS listener integration.nacelle: convenience crate that re-exports the split crates and owns the reference length-delimited protocol.
The reference protocol intentionally stays out of nacelle-core and
nacelle-tcp; it is a batteries-included implementation exported by the
umbrella nacelle crate.
App Core And Protocol Adapters
Nacelle is organized so application behavior lives behind the Handler
boundary. A handler receives a transport-neutral NacelleRequest and returns a
NacelleResponse.
TCP Protocol implementations are adapters: they decode a wire format into
request metadata and encode responses back into frames. Swapping protocols
should not require rewriting the app core. The app-first serving path wires
those pieces together with NacelleApp, NacelleProtocols, and
NacelleApp::serve(...); lower-level TcpServer and NacelleHost APIs remain
available for services that need direct listener control.
TLS lives in nacelle-core because the configuration and provider metadata are
shared. tls is provider-neutral. rustls enables the Rustls provider used by
HTTP and TCP. openssl enables the OpenSSL provider for TCP without
selecting Rustls. Both providers feed NacelleTlsProvider and per-connection
TLS metadata.
Request Flow
listener
-> connection limit
-> connection task
-> protocol/HTTP decode
-> request limit
-> handler
-> response body encode/stream
TCP and Unix socket listeners use the nacelle-tcp Protocol<Req> trait to
decode request heads and encode response frames. HTTP uses nacelle-http with
Hyper HTTP/1 and maps requests into the same NacelleRequest /
NacelleResponse shape.
NacelleRequest::connection carries transport, a stable connection id, peer
address, local address, local Unix socket path, effective peer IP, TLS metadata,
and an optional typed extension. Raw protocol servers can populate that
extension with connection_extension_factory(...) for auth/session state
derived at accept or handshake time. Apps using serve(protocols, app) can set
the same extension factory on NacelleApp.
HTTP-specific edge policy remains in nacelle-http: Host, method, URI/header
shape checks, per-peer request rate limits, access logging, and security header
injection. TCP keeps protocol semantics in the protocol implementation and
shared lifecycle/limit enforcement in core.
Runtime State
NacelleRuntimeState owns shared budgets and counters. Connection, request, and
streaming-task limits are non-blocking atomic bounded counters. Memory uses a
checked allocation guard that releases on drop.
This keeps the common request path allocation-light while still enforcing bounded defaults.
Bodies
NacelleBody has three internal shapes:
- empty/single chunk for fast small responses
- buffered chunks for decoded TCP bodies already in memory
- streaming channel for request/response bodies that move asynchronously
TCP large request bodies reserve their declared length while streaming. HTTP
request bodies reserve Content-Length when Hyper exposes a bounded size hint.
TCP protocols can override RequestMetadata::max_body_bytes(...) to choose a
phase-aware body limit immediately after head decoding and before body buffering
or streaming begins.
Shutdown
Listeners own a JoinSet of accepted connection tasks. Shutdown proceeds in
stages:
- signal shutdown
- stop accepting
- drain active connection tasks
- abort remaining tasks after the drain deadline
- emit shutdown telemetry
Task tracking is at the connection boundary, not the per-request hot path.
Observability
Telemetry is deliberately low-cardinality. Reasons are static strings such as
connections, request_body_bytes, or http_body_read.
With otel, runtime gauges are observable instruments backed by runtime-state
atomics, so collection reads current values without per-request metric writes.
NacelleTelemetry owns lifecycle, request, phase, error, and byte metrics for
all transports. Transports that can provide extra low-cardinality detail attach
a NacelleMetricsContext with listener, protocol, transport, and TLS labels.
Request metric switches live under NacelleTelemetryConfig::request_metrics.
Started/completed counters and byte counters are on by default; in-flight
counters, duration histograms, and phase histograms are disabled by default.
Enable them deliberately with NacelleTelemetry::default() builder methods on
the server or app when you need diagnostic detail and can afford the extra
per-request metric writes. Core/HTTP request paths do not start a request timer
unless duration metrics or HTTP access logging are enabled.