Skip to content

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 ​

yaml
schemaVersion: "7.1"
listeners:
  - host: "127.0.0.1"
    port: 8000
    decryptSsl: false
    forwardHost: "127.0.0.1"
    forwardPort: 8080
shell
titanium test -c twp.yaml
titanium run -c twp.yaml

forwardHost / 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 ​

yaml
schemaVersion: "7.1"
listeners: []
routes: []
clusters: []
staticFiles: null
plus: null
certificates: null
logging: null
server: null

Engine knobs live under server: (reference). Plus feature options stay under plus: / plus.options — Plus does not configure the engine.

Listeners ​

FieldTypeNotes
hoststringBind 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.
portintDefault 8000. 0 is an OS-chosen (ephemeral) port. Inspector toolbar * / empty means the same.
decryptSslboolTerminate TLS / decrypt HTTPS (man-in-the-middle when used for MITM)
typestring?explicit, transparent, socks, or quic (null uses ForwardHost heuristics)
forwardHost / forwardPortstring / intClassic single-origin reverse (no route table)
enableHttp2bool?false forces H1 globally; null inherits server / proxy default
enableHttp3bool?Per-listener; any false disables global H3
maxCachedConnectionsint?Per-endpoint pool depth
maxConcurrentClientsint?Per-endpoint admission cap
genericCertificateNamestring?Fallback hostname when SNI is absent
maxInboundBidirectionalStreams / maxInboundUnidirectionalStreamsint?QUIC stream caps
handshakeTimeoutSeconds / idleTimeoutSecondsint?QUIC timeouts

Routes & clusters ​

json
{
  "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:

KindParametersEffect
PathRemovePrefixprefixStrip a path prefix
PathPrefixprefixPrepend a path prefix
QueryValueSetname, valueSet or replace a query parameter
RequestHeaderSetname, valueSet a request header
RequestHeaderRemovenameRemove a request header
ResponseHeaderSetname, valueSet a response header after the origin responds
ResponseHeaderRemovenameRemove a response header after the origin responds
json
"transforms": [
  { "kind": "PathPrefix", "parameters": { "prefix": "/gw" } },
  { "kind": "QueryValueSet", "parameters": { "name": "env", "value": "lab" } },
  { "kind": "RequestHeaderSet", "parameters": { "name": "X-Edge", "value": "1" } }
]

Static files ​

yaml
staticFiles:
  root: "./www"
  enableGzip: true
  enableBrotli: false

Certificates / ACME ​

Automatic Certificate Management Environment (ACME) can obtain Let’s Encrypt (or other directory) certificates for public listeners:

yaml
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 ​

yaml
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: null

Plus ​

yaml
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 ​

shell
titanium test -c twp.yaml

Server reference ​

Null nested objects and null properties leave the library or profile default. Apply order: profile first, then overlays.

yaml
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.

yaml
server:
  accessLog:
    path: "./logs/access.ndjson"
    sampleRate: 1.0   # 0.0–1.0; omit for 1.0

Each 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:

shell
titanium reload -c twp.yaml
WhatLive reload?
Routes, transforms, clusters / destinations, LB algorithmYes
server: timeouts, pooling, protocol flags, decrypt skip hostsYes
Listeners (bind / port / type)No — restart
Certificates / ACMENo — restart
Plus plugins (WAF, JWT, gRPC-JSON, discovery options)No — restart
Static files, access log pathNo — restart
HowNotes
titanium reload -c <config>All OS (Windows named event; Unix SIGHUP)
Unix SIGHUP / systemd systemctl reloadUnit includes ExecReload=/bin/kill -HUP $MAINPID
titanium run --watchDebounced file watch (optional)
Plus PUT /v1/snapshotProgrammatic 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 on ProxyServer)
  • WinAuthCredentialsProvider
  • GetCustomUpStreamProxyFunc, CustomUpStreamProxyFailureFunc
  • ShouldInterceptHttp
  • BufferPool, Logging.LoggerFactory, custom CertificateStorage

See also ​

Core, CLI, and website: MIT