Table of Contents
- Operations, Limits, and Security
Operations, Limits, and Security
TrevRPC exposes explicit runtime knobs instead of hiding transport behavior. Production deployments should configure TLS, transport negotiation, deadlines, concurrency limits, authorization, and metrics deliberately.
TLS and Transport Negotiation
Native TrevRPC over QUIC requires TLS with ALPN trevrpc/1.
Rust:
server_crypto.alpn_protocols = vec![trevrpc::ALPN.to_vec()];
client_crypto.alpn_protocols = vec![trevrpc::ALPN.to_vec()];
Go:
tlsConfig := &tls.Config{
NextProtos: []string{trevrpc.ALPN},
}
Kotlin Netty:
val clientTls = NettyClientTls(
serverName = "api.example.com",
trustManagerFactory = trustManagerFactory,
)
val serverTls = NettyServerTls.Pem(keyFile, certificateFile)
Ordinary HTTP/3 and WebTransport use ALPN h3 instead of trevrpc/1. In Go, advertise http3.NextProtoH3; WebTransport additionally enables QUIC datagrams through QUICServerConfig. In Rust, advertise trevrpc::HTTP3_ALPN; combined endpoints advertise both trevrpc::ALPN and trevrpc::HTTP3_ALPN. See HTTP/3 and WebTransport.
The examples use local self-signed certificates for convenience. Real deployments should use real certificates or mTLS configured through Quinn, rustls, crypto/tls, quic-go, MsQuic, or Netty. C, Node, and Kotlin Netty clients validate certificates by default. Kotlin's insecureDevTrust and disabled hostname verification, and C/Node's skip_certificate_validation / skipCertificateValidation, are only for local development and tests. Configure explicit private-CA trust instead in production.
Request Metadata Authorization
TrevRPC supports request-level authorization through metadata.
Rust bearer-token authorizer:
server.set_authorizer(trevrpc::server::MetadataValueAuthorizer::bearer(
"expected-token",
));
Go bearer-token authorizer:
server.SetAuthorizer(trevrpc.BearerAuthorizer("expected-token"))
Kotlin authorizer:
val server = Server(
authorizer = Authorizer { request ->
if (!request.metadata.contains("authorization")) {
throw TrevRpcException(Status.unauthenticated("missing bearer token"))
}
},
)
Attach metadata from clients:
let options = trevrpc::client::CallOptions::new()
.with_metadata("authorization", b"Bearer expected-token".to_vec());
response, err := client.SayHello(
ctx,
request,
trevrpc.WithMetadata("authorization", []byte("Bearer expected-token")),
)
const reply = await client.sayHello(request, {
metadata: { authorization: "Bearer expected-token" },
});
val options = CallOptions(
metadata = Metadata.of(
"authorization" to "Bearer expected-token".encodeToByteArray(),
),
)
Authorization runs after metadata and protocol validation but before route lookup. This avoids leaking whether a method exists to unauthenticated callers.
Metadata Limits
| Limit | Value |
|---|---|
| Entries | 64 |
| Key length | 128 bytes |
| Value length | 8 KiB |
| Total size | 64 KiB |
Use lowercase metadata keys made from ASCII letters, digits, ., _, or -. Do not use the reserved trevrpc- prefix.
Client Defaults
| Setting | Default |
|---|---|
| Timeout | None |
| Max unary response body | 4 MiB |
| Max response messages | 4096 |
| Max cumulative response stream body | 16 MiB |
| Stream idle timeout | 30 seconds |
| Metadata | Empty |
Set per-call deadlines by default for production clients. Leaving timeouts unset can allow calls to wait indefinitely if the transport remains open but progress stops outside the stream idle timeout path.
Kotlin uses these defaults in CallOptions. Nullable timeout and stream message/body limits disable the corresponding optional bound; maxResponseBodySize remains a non-negative numeric limit.
Server Defaults
| Setting | Default |
|---|---|
| Max frame size | 4 MiB |
| Max concurrent connections | 256 |
| Max concurrent streams per connection | 64 |
| Max concurrent requests | 1024 |
| Graceful shutdown timeout | 30 seconds |
| Initial request timeout | 10 seconds |
| Max stream messages | 4096 |
| Max cumulative stream body | 16 MiB |
| Stream idle timeout | 30 seconds |
| Ordinary HTTP/3 | Disabled |
| HTTP/3 path | /trevrpc |
Tune these based on expected payload size, number of clients, streaming duration, and memory budget.
Kotlin uses these values in core ServerOptions. Transport selection is separate: NettyRpcServerConfig enables native QUIC and ordinary HTTP/3 by default, while WebTransport remains opt-in and requires admission.
Receive Memory Budget
Treat the receive budget as the product of frame size, stream concurrency, and cumulative body limits. Wider QUIC flow-control windows are useful only when the application can afford the memory held by slow readers and stalled handlers.
Production defaults are intentionally bounded:
| Budget Input | Operational Meaning |
|---|---|
| Max frame size | Minimum per-stream receive window needed for one complete TrevRPC frame |
| Max concurrent streams per connection | Upper bound for simultaneously buffered request streams on one transport connection |
| Max concurrent requests | Upper bound for handlers that can retain decoded request or response state |
| Max cumulative stream body | Per-RPC cap for request and response body bytes over streaming calls |
| Stream idle timeout | Time bound for stalled streaming progress |
Do not widen transport receive windows or stream concurrency just for benchmarks without a matching connection-level memory envelope. Benchmark-specific profiles should be labeled separately from production defaults and should include slow-reader or overload tests proving memory remains bounded.
Current Go, Rust, and Kotlin Netty QUIC helpers derive conservative receive windows from frame size, stream body size, and stream concurrency. They cap or explicitly size transport windows rather than exposing an unsafe “make buffers larger” shortcut.
Additional Resource Budget API Decision
No additional public resource-budget fields are justified by the corrected high-level C evidence. The existing controls bound every measured retained-data dimension, and a new field would either duplicate one of those limits or add an aggregate byte pool without a workload demonstrating that the coarser concurrency-times-per-stream envelope is insufficient.
| Candidate | Existing controls | Evidence and decision |
|---|---|---|
| Request-body budget | Encoded max_frame_size, server stream message count, cumulative max_stream_body_size, stream/request admission, and idle timeout |
No new field. Twelve stalled handlers accepted and drained 2 MiB of 64 KiB request messages per stream, with 4 KiB responses and no reader progress during the hold. Exact cumulative exhaustion is already enforced at the configured boundary. |
| Response-body budget | Per-call unary response-body limit, per-call response message/body limits, server cumulative stream-body limit, encoded frame limit, and response idle timeout | No new field. Eight slow readers received 1 MiB each in 64 KiB response messages after 4 KiB requests while draining only every 256 KiB. Direction-specific client response limits already provide the useful asymmetric control. |
| Streaming-message byte budget | Encoded frame limit plus stream message-count and cumulative-body limits | No new field. The matrix exercised request sizes from 4 KiB through 256 KiB and response sizes from 4 KiB through 64 KiB. C, Go, Rust, JavaScript, and Kotlin retain exact-limit/one-over frame, message-count, unary-response, and cumulative-body tests. |
| Combined total-stream byte budget | Per-direction cumulative stream-body limits, message-count limits, frame limit, and deadline/idle timeout | No new field. Eight streams accepted exactly 1 MiB and rejected the next 64 KiB frame with ResourceExhausted; the stalled-handler case also completed 2 MiB per stream. A combined request-plus-response pool would be useful only for an unproven mixed full-duplex workload. |
| Connection receive byte budget | Per-stream frame/body caps multiplied by admitted stream concurrency, total request admission, connection limit, and idle timeout | No new field. The overload case admitted exactly 8 of 16 requests and rejected 8, while the 12-stream stalled-ingress and 8-stream slow-reader cases stayed within explicit aggregate RSS and cleanup-convergence bounds. |
The corrected 2026-07-09 matrix covered 54 isolated processes: three transport profiles, six workload shapes, and three runs. Every process passed its exact byte/status invariants, peak bound, post-cleanup convergence bound, and zero-send-failure check. This is historical evidence from that source snapshot; the exact commands, rows, and per-profile peak medians remain available in git history.
This decision does not claim an exact process-memory cap: allocator, protobuf, handler, and transport overhead remain workload-dependent. Operators that need a lower connection envelope should lower per-stream body limits, stream/request concurrency, or both. Reconsider an aggregate byte pool only with a measured workload that needs many admitted streams with heterogeneous body ceilings and cannot be represented safely by those controls.
C native MsQuic sends keep send buffering disabled by default. Any frame header, protobuf prefix/suffix, or body buffer submitted to MsQuic StreamSend must remain valid until the stream reports SEND_COMPLETE, including canceled completions after reset. The C runtime's direct unary response and terminal status paths wait for pending send completion before freeing or resetting borrowed response/status frame parts; stream close also drains pending sends before releasing send state.
Retired C Large-Buffer Profile
The experimental named C throughput-1m profile and its profile-application helpers were retired. No named large-buffer profile is exposed. The raw low-level stream_recv_window, conn_flow_control_window, msquic_send_buffering_enabled, and msquic_execution_profile fields remain available for advanced, explicit configuration; normal C defaults are unchanged.
The measurements that motivated and validated the former profile remain available in git history. They describe the source snapshots that produced those results, not an active API recommendation. MsQuic receive windows are transport flow-control settings rather than an aggregate process-memory limit, so explicit tuning still requires matching stream, frame, cumulative-body, timeout, and admission limits.
The C server uses one MsQuic listener for native TrevRPC and HTTP/3 ALPNs. Ordinary HTTP/3 and WebTransport share the H3 connection dispatcher. max_streams_per_session can therefore raise the shared listener's transport peer-bidirectional-stream setting above peer_bidi_stream_count; application admission remains separately bounded by trevrpc_server_options.max_concurrent_streams_per_connection.
C ABI Compatibility
The current pre-stable C ABI is v4, exposed as TREVRPC_C_ABI_VERSION == 4 and trevrpc_c_abi_version() == 4. ABI v4 adds ordinary HTTP/3 server configuration, admission, stream adapters, and TREVRPC_TRANSPORT_KIND_HTTP3.
The preceding ABI v3 cleanup removed the named MsQuic tuning-profile enum, constants, and application functions. It also removed max_data_per_session and handshake_timeout_ms from trevrpc_server_config, and removed max_data_per_session, stream_recv_window, conn_flow_control_window, and handshake_timeout_ms from trevrpc_wt_config. The client and shared-server configuration structs still expose their raw low-level MsQuic receive-window, execution-profile, and send-buffering fields.
Adding or removing fields changes struct sizes and, for later members, offsets. Every C object and generated or native binding linked to the changed library must therefore be rebuilt together. Reusing objects from an earlier ABI with ABI v4 is unsupported.
Limit Disable Semantics
Each language uses its native option shape for disabling limits:
| Runtime | Disable style |
|---|---|
| C | Signed limit fields use negative values to disable; zero-valued call-option fields mean the runtime default |
| Go | Negative call-option values and Without* helpers disable response limits |
| Rust | Option::None disables optional stream/message limits |
| Kotlin | null disables optional duration, concurrency, message-count, and cumulative-body limits |
| JavaScript | Negative response stream limits disable stream message/body caps |
Load Shedding
Current native QUIC, HTTP/3, and WebTransport servers enforce concurrency with semaphores or counters:
- Too many connections are refused or closed.
- Too many streams per connection return
Unavailable. - Too many total concurrent requests return
Unavailable. - Oversized frames or stream bodies return
ResourceExhausted.
Prefer explicit limits. Unbounded streams, unlimited message counts, or disabled idle timeouts should only be used when another layer enforces equivalent bounds.
Graceful Shutdown
Rust exposes serve_quinn_with_shutdown, which stops accepting new work when the shutdown future completes, drains active streams, and force-closes after the configured graceful shutdown timeout.
Rust combined native QUIC, HTTP/3, and WebTransport servers expose serve_quinn_and_webtransport_with_shutdown with the same shutdown model. HTTP/3-only servers can use serve_http3_with_shutdown; WebTransport-only servers can use serve_webtransport_with_shutdown.
Go ServeQUIC takes a context for native QUIC, HTTP/3, and WebTransport. Cancel the context to stop accepting and drain according to GracefulShutdownTimeout.
Kotlin NettyRpcServer.shutdown() stops admission, calls the core Server.shutdown() drain path, closes active QUIC connections and the UDP listener, and releases event-loop resources. Repeated shutdown calls are safe.
Metrics
C, Go, Kotlin, Rust, and Node native expose small metrics or lifecycle hooks.
Metrics events include:
- Service name.
- Method name.
- Request body length.
- Response body length.
- Final status code.
- Elapsed duration.
Metrics callbacks run inline on RPC tasks. Keep them non-blocking.
Tracing
The Rust crate's optional tracing Cargo feature emits structured lifecycle events and Quinn frame events through the trevrpc::quinn::frames target. Applications enable the feature and configure a tracing subscriber and filter. The former TREVRPC_RUST_QUINN_FRAME_TRACE stderr switch was removed.
Focused diagnostics remain available:
| Runtime | Diagnostic Hook | Notes |
|---|---|---|
| C / MsQuic | TREVRPC_C_FRAME_TRACE=1 |
Emits wire-level frame direction, type, kind, status, body length, and encoded length to stderr. Payload bytes and metadata values are not logged. |
| Rust / Quinn frames | Cargo feature tracing |
Emits structured Quinn transport frame, FIN, reset, and STOP_SENDING events through tracing; there is no Rust frame-trace environment switch. |
| Rust / Quinn qlog | TREVRPC_RUST_SPLIT_BENCH_QUINN_QLOG=/path/to/trace.qlog |
Writes Quinn packet sent/received/lost and recovery events. The recorded Quinn 0.11.11 ACK captures had no frame arrays and emitted empty group_id values; that is an observation of those captures, not a universal Quinn qlog guarantee. Combine qlog with protocol trace when decoded frame identity is required. |
| Rust / Quinn protocol | TREVRPC_RUST_SPLIT_BENCH_QUINN_PROTO_TRACE=1 |
Emits timestamped Quinn packet-number spans and decoded QUIC frames. This is substantially more timing-invasive than qlog alone. |
| Rust / Quinn TLS | SSLKEYLOGFILE=/path/to/keylog |
Writes TLS secrets from the Rust diagnostic executable for packet decryption, including its WebTransport server mode. Treat the file as secret material. |
Frame tracing, qlog, protocol tracing, and key logging perturb timing and can expose sensitive traffic. Use them for focused diagnostic runs only, and do not publish captured or traced runs as normal performance rows. Historical benchmark commands retain the diagnostic controls accepted by their recorded source snapshots.
Non-Cooperative Work Boundary
Deadlines, cancellation, graceful shutdown, stream resets, and terminal statuses are enforced at transport and runtime boundaries. They cannot forcibly stop CPU-bound user code or custom async iterables that never yield.
Operationally:
- Keep handlers and stream iterables cancellation-aware.
- Keep request and stream permits tied to work that is still running or explicitly isolate abandoned work.
- Use graceful shutdown timeouts as hard service-level bounds, not as proof that every user task was synchronously joined.
- Test local deadline/cancellation racing remote terminal statuses for every generated streaming shape used in production.
Error Mapping
Important operational statuses:
| Status | Typical cause |
|---|---|
InvalidArgument |
Bad metadata, malformed request body, wrong streaming kind |
DeadlineExceeded |
Client or server deadline expired |
ResourceExhausted |
Frame, message count, or stream body limit exceeded |
Unauthenticated |
Metadata authorizer rejected the request |
Unimplemented |
Unknown service or method |
Internal |
Protocol violation, decode failure in unexpected path, handler bug |
Unavailable |
Concurrency limit, connection issue, transport unavailable |
Deployment Checklist
- Configure TLS certificates and the selected transport negotiation.
- Decide whether client identity belongs in TLS, metadata, or both.
- Set client call deadlines.
- Set server concurrency and stream limits.
- Configure authorization before registering public endpoints.
- Install non-blocking metrics.
- Test graceful shutdown with active unary and streaming calls.
- Exercise oversized payload, invalid metadata, unauthenticated, and deadline paths.