Configuration (twp.yaml)
Native schema version 7.1. For CLI operators: start with a minimal reverse file, then add routes, certificates, or Plus as needed.
Next: copy the minimal reverse below → titanium test -c twp.yaml → titanium run -c twp.yaml.
Minimal reverse
schemaVersion: "7.1"
listeners:
- host: "127.0.0.1"
port: 8000
decryptSsl: false
forwardHost: "127.0.0.1"
forwardPort: 8080titanium test -c twp.yaml
titanium run -c twp.yamlforwardHost / forwardPort is the classic single-origin reverse (no route table). For path-based routing and load balancing, add routes and clusters below.
Top-level shape
schemaVersion: "7.1"
listeners: []
routes: []
clusters: []
staticFiles: null
plus: null
certificates: null
logging: null
server: nullEngine knobs live under server: (reference). Plus feature options stay under plus: / plus.options — Plus does not configure the engine.
Listeners
| Field | Type | Notes |
|---|---|---|
host | string | Bind address. Default 0.0.0.0 (all IPv4 interfaces). * is the same as 0.0.0.0. Use 127.0.0.1 for localhost only. |
port | int | Default 8000. 0 is an OS-chosen (ephemeral) port. Inspector toolbar * / empty means the same. |
decryptSsl | bool | Terminate TLS / decrypt HTTPS (man-in-the-middle when used for MITM) |
type | string? | explicit, transparent, socks, or quic (null uses ForwardHost heuristics) |
forwardHost / forwardPort | string / int | Classic single-origin reverse (no route table) |
enableHttp2 | bool? | false forces H1 globally; null inherits server / proxy default |
enableHttp3 | bool? | Per-listener; any false disables global H3 |
maxCachedConnections | int? | Per-endpoint pool depth |
maxConcurrentClients | int? | Per-endpoint admission cap |
genericCertificateName | string? | Fallback hostname when SNI is absent |
maxInboundBidirectionalStreams / maxInboundUnidirectionalStreams | int? | QUIC stream caps |
handshakeTimeoutSeconds / idleTimeoutSeconds | int? | QUIC timeouts |
Routes & clusters
{
"schemaVersion": "7.1",
"listeners": [
{ "host": "127.0.0.1", "port": 8000, "decryptSsl": false }
],
"routes": [
{
"id": "r1",
"clusterId": "c1",
"order": 1,
"match": { "path": "/", "pathKind": "Prefix" }
}
],
"clusters": [
{
"id": "c1",
"algorithm": "RoundRobin",
"destinations": [
{ "id": "d1", "address": "127.0.0.1", "port": 8080 }
]
}
]
}Match fields typically include host, path (Exact / Prefix / Template), method, headers, and query. Cluster algorithms include RoundRobin, Random, LeastRequests, and LeastTime; destinations support weight and sticky cookie/header. When any cluster uses LeastTime, the CLI automatically enables request timing capture so latency EWMA can drive selection.
Optional limits on a route overrides that session only: requestTimeoutSeconds, idleTimeoutSeconds, maxBufferedBodyBytes, maxWebSocketFramePayloadBytes. Omit it and the server limits apply. Full key list: Limits and escape hatches.
Route transforms
Optional transforms on a route rewrite the upstream request (and can stage response header changes). Empty/absent transforms keep the reverse fast path. Supported kinds:
| Kind | Parameters | Effect |
|---|---|---|
PathRemovePrefix | prefix | Strip a path prefix |
PathPrefix | prefix | Prepend a path prefix |
QueryValueSet | name, value | Set or replace a query parameter |
RequestHeaderSet | name, value | Set a request header |
RequestHeaderRemove | name | Remove a request header |
ResponseHeaderSet | name, value | Set a response header after the origin responds |
ResponseHeaderRemove | name | Remove a response header after the origin responds |
"transforms": [
{ "kind": "PathPrefix", "parameters": { "prefix": "/gw" } },
{ "kind": "QueryValueSet", "parameters": { "name": "env", "value": "lab" } },
{ "kind": "RequestHeaderSet", "parameters": { "name": "X-Edge", "value": "1" } }
]Static files
staticFiles:
root: "./www"
enableGzip: true
enableBrotli: falseCertificates / ACME
Automatic Certificate Management Environment (ACME) can obtain Let’s Encrypt (or other directory) certificates for public listeners:
certificates:
certificatePath: "./certs/fullchain.pem"
privateKeyPath: "./certs/privkey.pem"
acmeEmail: "ops@example.com"
acmeDomain: "app.example.com"
acmeDirectory: "https://acme-v02.api.letsencrypt.org/directory"Use real paths and emails in your environment. Do not commit private keys. MITM engine knobs are under server.certificateManager; this section is for listener leaf PEM/PFX and ACME.
Logging
logging:
enabled: true
minimumLevel: "Error" # Error shows limit rejections. Use Debug, or titanium run -v, for Observe lines.
enableConsole: true
enableConsoleColors: true
enableFile: false
filePath: null
maxFileSizeBytes: null
maxRolledFiles: null
queueCapacity: nullPlus
plus:
enabled: true
controlPlane:
host: "127.0.0.1"
port: 9080
sharedSecret: "<shared-secret>"
options:
cache.enable: "true"Common plus.options keys (string values): see the table on Plus (discovery.*, security.*, waf.*, state.*, resilience.*, cache.enable, grpc.transcode.*). Enabling grpc.transcode.enabled forces the HTTP session interception path — see gRPC-JSON transcoding. Engine settings belong in server:, not plus.options.
Validate
titanium test -c twp.yamlServer reference
Null nested objects and null properties leave the library or profile default. Apply order: profile first, then overlays.
server:
profile: Balanced # Balanced | LegacyCompatible | PublicFacing. PublicFacing sets streams to 256, idle 60s, request 120s.
enableHttp2: true
enableHttp3: null
enableRfc8441: false
enableQpackDynamicTable: false
enableHttpsSvcbDnsDiscovery: null
enable100ContinueBehaviour: false
compatibilityMode100Continue: false
enableWinAuth: false
originHttpVersionPolicy: PreserveClientVersion # or NormalizeToHttp11
viaHeaderPseudonym: "titanium-web-proxy"
blockPrivateNetworkDestinations: false
blockLoopbackDestinations: false
checkCertificateRevocation: NoCheck
dnsServerEndPoint: "8.8.8.8:53"
accessLog:
path: "logs/access.ndjson" # omit or null = off (zero cost)
sampleRate: 1.0 # 0.0–1.0
timeouts:
connectionTimeOutSeconds: 60
connectTimeOutSeconds: 20
clientHeaderTimeoutSeconds: 0
responseHeaderTimeoutSeconds: 0
idleReadTimeoutSeconds: 0
idleWriteTimeoutSeconds: 0
requestTimeoutSeconds: 0
networkFailureRetryAttempts: 1
pooling:
enableConnectionPool: true
enableTcpServerConnectionPrefetch: true
enableIpv6UnreachableSoftSkip: true
maxCachedConnections: 128
maxConcurrentHttp11HttpsOriginCreates: null
maxConcurrentClientConnections: null
noDelay: true
enableTcpKeepAlive: true
tcpTimeWaitSeconds: 0
listenerBackLog: 1024
reuseSocket: true
threadPoolWorkerThread: null
limits:
maxHeaderLineBytes: 65536
maxHeaderCount: 256
maxHeaderAggregateBytes: 262144
maxEncodedBodyBytes: null
maxDecodedBodyBytes: null
maxDecompressionRatio: 200
maxConcurrentClients: null
maxConcurrentStreamsPerConnection: 1000 # PublicFacing profile uses 256
maxPeerInitiatedIncompleteStreamResets: 100
maxOpenHeaderBlockFrames: 128
maxOpenHeaderBlockDurationSeconds: 10
connectionPoolingEnabled: true
maxCachedConnectionsPerHost: 128
maxOriginHttp2ConnectionsPerAuthority: 1
maxCertificateCacheEntries: 1024
maxCertificateDiskCacheEntries: null
maxBufferedBodyBytes: 4194304
maxDecodedHeaderListBytes: 65536
maxWebSocketFramePayloadBytes: 16777216
maxHttp3FramePayloadBytes: 4194304
maxDeferredOutboundBytesPerStream: 4210688
maxTrailerHeaderCount: 100
maxTrailerHeaderBlockBytes: 16384
maxHttp2CompressedHeaderBlockBytes: 262144
maxInterimResponses: 20
maxAuthChallengeRounds: 3
maxUpstreamProxyAuthenticationAttempts: 5
maxWinAuthTokenBytes: 12288
http2WindowUpdateTimeoutSeconds: 60
policyModes:
bodyBudget: Enforce
decompressionRatio: Enforce
headerLimits: Enforce
admissionControl: Enforce
http2AbuseBudget: Enforce
webSocketFrameBudget: Enforce
http1ReplaySafety: Observe # PublicFacing keeps Enforce unless you set this
allowAmbiguousFraming: false
tls:
supportedSslProtocols: [Tls12, Tls13]
supportedServerSslProtocols: [None]
upstream:
forwardToUpstreamGateway: false
upstreamProxyConfigurationScript: null
httpProxy:
hostName: "proxy.example"
port: 8080
proxyType: Http
useDefaultCredentials: false
bypassLocalhost: false
proxyDnsRequests: false
nextHop: null
httpsProxy: null
upStreamEndPoint: null
upStreamEndPointIPv4: null
upStreamEndPointIPv6: null
decryptSkipHosts: [] # present ⇒ Replace tunnel-only list (omit key to keep Merge factory defaults)
decryptOnlyHosts: [] # when non-empty, only these hosts are decrypted (Replace with skip/only)
# enableDecryptFailureBypass, decryptFailureBypassTtlMinutes, decryptFailureBypassMaxEntries,
# decryptFailureBypassThreshold, and proxyAuthenticationRealm are server keys. See Limits.
systemProxyBypassHosts: null # present ⇒ Replace OS bypass (omit ⇒ Merge identity defaults); SSO risk if removed
proxyLoopback: true # localhost via proxy when building SystemProxySettings from config
certificateManager:
certificateEngine: BouncyCastleFast
leafCertificateKeyAlgorithm: EcdsaP256
pfxFilePath: null
pfxPassword: null
overwritePfxFile: true
certificateValidDays: 396
certificateGraceDays: 2
certificateCacheTimeOutMinutes: 60
rootCertificateName: null
rootCertificateIssuerName: null
saveFakeCertificates: true
disableWildCardCertificates: false
accessLog: null # opt-in NDJSON access log (see below)Listener-level enableHttp2: false still forces HTTP/2 off after server.enableHttp2. Listener-level enableHttp3: false (or server.enableHttp3: false) disables HTTP/3.
HTTP interception is not a YAML knob: the CLI turns it on automatically when the config needs the session path (transforms, static files, ACME, access logs, or gRPC-JSON). Request timing capture is not a YAML knob either: it is enabled automatically when any cluster uses LeastTime (and when access logs are enabled).
Access log (server.accessLog)
Opt-in JSON Lines (NDJSON) access log written after each response. Bodies are never buffered for this feature.
server:
accessLog:
path: "./logs/access.ndjson"
sampleRate: 1.0 # 0.0–1.0; omit for 1.0Each line includes ts, method, url, host, status, durationMs, and clientIp. Omit accessLog (or leave path empty) for zero cost on the hot path.
Graceful reload
Change routes and backends without dropping connections:
titanium reload -c twp.yaml| What | Live reload? |
|---|---|
| Routes, transforms, clusters / destinations, LB algorithm | Yes |
server: timeouts, pooling, protocol flags, decrypt skip hosts | Yes |
| Listeners (bind / port / type) | No — restart |
| Certificates / ACME | No — restart |
| Plus plugins (WAF, JWT, gRPC-JSON, discovery options) | No — restart |
| Static files, access log path | No — restart |
| How | Notes |
|---|---|
titanium reload -c <config> | All OS (Windows named event; Unix SIGHUP) |
Unix SIGHUP / systemd systemctl reload | Unit includes ExecReload=/bin/kill -HUP $MAINPID |
titanium run --watch | Debounced file watch (optional) |
Plus PUT /v1/snapshot | Programmatic route/cluster swap without editing the file |
Invalid reloads keep the previous routes and clusters; the process stays up. Unlike a typical YARP process recycle, Titanium keeps listeners bound and in-flight sessions on the old cluster snapshot until they finish.
Code-only callbacks
These cannot be set from YAML; wire them in Library / C# code:
ProxyBasicAuthenticateFunc,ProxySchemeAuthenticateFunc(and related realm/schemes onProxyServer)WinAuthCredentialsProviderGetCustomUpStreamProxyFunc,CustomUpStreamProxyFailureFuncShouldInterceptHttpBufferPool,Logging.LoggerFactory, customCertificateStorage