Network Listeners Configuration Reference
A listener is one socket Bondy binds — a TCP port, a TLS port, or a Unix domain socket — together with the protocol it speaks and, for HTTP, the set of services it exposes on that socket. Every listener an operator declares lives under listeners.$name.* in bondy.conf, where $name is a name of the operator's choosing, e.g. listeners.public_http.transport = tcp.
A node with no listeners.* key at all starts three listeners, all plaintext:
| Name | Transport | Protocol | Port | Services |
|---|---|---|---|---|
admin | tcp | http | 18081 | admin_api, wamp_ws, admin, metrics (start_phase = early) |
api_gateway_http | tcp | http | 18080 | api_gateway, wamp_ws, wamp_sse, wamp_longpoll |
wamp_tcp | tcp | wamp_rawsocket | 18082 | — |
Declaring even one listeners.* key replaces that set: the node then starts the declared listeners and the reserved admin listener, and nothing else. A template being converted needs every listener it wants restated here, not just the one being changed.
The pre-1.0 keys are gone
The per-scheme keys that configured Bondy's fixed listeners — admin_api.{http,https}.*, api_gateway.{http,https}.*, wamp.{tcp,tls}.*, bridge.listener.{tcp,tls}.* and the global wamp.{websocket,sse,longpoll}.* carrier keys — have been removed. Nothing reads them, and a file that still sets them loses every one of those settings silently. See Migrating from the pre-1.0 keys below.
None of the listeners.$name.* keys has a schema-supplied default. A key you do not set falls back to a built-in default that depends on the listener's transport and protocol; the values below marked as defaults are those built-ins, and they are documented here because they are deliberately not written into the generated etc/bondy.conf.
Identity and mount
Every listener needs transport, protocol and a bind target (port or path); an HTTP listener additionally needs services.
listeners.public_wamp.transport = tcp
listeners.public_wamp.protocol = wamp_rawsocket
listeners.public_wamp.port = 18082The socket driver. TLS is a transport, not a protocol: an HTTPS listener is protocol = http with transport = tls, not a distinct HTTP variant.
What frames the wire: http (multiplexing several services by path), or a raw-socket protocol carrying exactly one thing — wamp_rawsocket for WAMP clients, bridge_relay for the socket a peer router's bridge connects to. A protocol Bondy has no handler for is refused at boot, naming the listener, rather than accepted and crashing at start.
The bind port, for a tcp or tls listener. There is no default — a default would let two listeners collide silently — so every listener has to state one. port = 0 asks the operating system to choose, which is the only value two listeners may share.
The socket file path, for a uds listener; the uds counterpart of port.
A comma-separated list of the services this listener exposes. Required when protocol = http and rejected for every other protocol: a raw socket carries one thing by construction, so a list of services on one would be ambiguous. services is what lets an HTTP listener multiplex several things onto one socket — the default api_gateway_http listener serves the API Gateway, WAMP-over-WebSocket, SSE and long-poll all on port 18080.
| Service | Carrier | Protocol | Notes |
|---|---|---|---|
api_gateway | api_gateway | — | Stored API Gateway specifications. |
admin_api | admin_api | — | The built-in Admin API (realms, users, grants). |
admin | — | — | Liveness/readiness endpoints (/ping, /ready). |
metrics | — | — | Prometheus scrape endpoint. |
wamp_ws | WebSocket | WAMP | |
bamp_ws | WebSocket | BAMP | Mounts /ws restricted to the bamp subprotocol. Bondy 1.0.0 has no BAMP wire implementation, so no session can be carried over it. |
wamp_sse | SSE | WAMP | |
wamp_longpoll | Long-poll | WAMP | |
mcp | MCP | MCP | The MCP Gateway endpoint. Mounts /mcp/realm/:realm and /.well-known/oauth-protected-resource/realm/:realm. Cannot share a listener with admin_api. |
An HTTP listener has to declare at least one service. Both a missing services key and an empty one are refused at boot, naming the listener. Such a listener would bind its socket and answer 404 to everything, with nothing reported anywhere. Note that listeners.<name>.services = with no value after it renders as an empty list, so it produces this error rather than a listener with some default set of services.
A listener can declare several services that share a carrier — wamp_ws and bamp_ws both mount on /ws — and they resolve to one route carrying both protocols rather than two routes competing for the same path. Declaring only one of them restricts that path to that subprotocol: a WAMP client offering wamp.2.json to a bamp_ws-only listener is refused with 400.
admin_api and api_gateway deliberately never share a listener in Bondy's own defaults: mounting a customer's stored API specification on the same socket that administers realms and grants would put it one misconfiguration away from being reachable by the wrong audience. Keep that split when declaring your own listeners. Declaring mcp and admin_api together on one listener is a boot error for the same reason.
Two services on one carrier do not collide, they shadow. A path claimed by two different carriers fails the dispatch build with route_collision — but only when both route sets are static. Once either side comes from an API Gateway specification it cannot: a specification arrives by replication after boot, so refusing it would take this node's dispatch table down over a document another node accepted. There the first route assembled answers the path, the other becomes unreachable on that listener, and a warning naming the path and host is logged. Bondy's own route sets are assembled first, so a specification cannot take over /ws, /ping, /ready or /metrics.
Whether the listener starts. A disabled listener is fully resolved and kept in the inventory — turning it on later runs the checks (TLS material included) that were deferred while it was off.
early binds the listener as soon as configuration is loaded, before the storage layer opens. Until the node's services start, an early listener mounts only the liveness (/ping) and readiness (/ready) routes, and only when it declares the admin service. Every other route on it, /metrics and /ws among them, answers 404 until the node's services start, when the full route set is mounted. An early listener without the admin service answers 404 to every path until then. The full route set is mounted before any normal listener binds, so the metrics scrape answers before the node serves WAMP sessions. Reaching ready means the normal phase finished binding, not that no client has connected yet — nothing gates connection acceptance on the node's readiness status.
| Path | Answers |
|---|---|
/ping | 204 whenever the listener is bound, including while storage is opening. |
/ready | 204 when the node is ready, 503 otherwise. GET only; any other method answers 405. |
Answer probes before the node is ready shows which probe uses each path.
Virtual hosts
An API Gateway specification's host field is honoured: a specification declaring "host": "api.example.com" answers only requests carrying that Host header, and "host": "_" answers on every host. Two specifications may declare the same path for different hosts.
Bondy's own paths — /ws, the SSE and long-poll endpoints, /ping, /ready, /cluster/topology, /metrics — are served on every host, including one a specification names. That is not automatic in Cowboy: its router commits to the first host entry whose host matches and never falls through to a later one, so a route mounted only on the wildcard host would be unreachable on any named host. Bondy copies its listener-wide routes into each named host to prevent that. If a specification declares one of those paths for its own host, the specification's route answers there and a warning is logged, because one of Bondy's endpoints is then not reachable on that host.
Taking a node out of rotation
Two WAMP procedures suspend and resume a phase at runtime, so a node can leave rotation without a restart or a configuration change:
bondy.listener.suspend("normal")
bondy.listener.resume("normal")The argument is a phase — early, normal or all — and both procedures are callable only from the master realm, because whether this node accepts connections is not a per-realm decision. Any other value is refused.
Suspending stops a listener accepting new connections and leaves established ones running, which is what makes this a drain: suspend, let the in-flight sessions finish, and the node has left rotation without dropping work. Nothing here stops a listener or closes a connection.
Suspending all includes the early phase, which carries /ping, /ready and /metrics — an orchestrator watching those will see the node as dead and may kill it. That is why the shutdown path suspends normal only. Resuming a phase that is already accepting is not an error.
Both procedures take a dry_run keyword argument. With dry_run = true, the procedure validates the phase and suspends or resumes nothing. The result carries dry_run: true, the phase, the listeners that the phase names, and a would sentence that describes the effect. No other procedure reports listener state, so a dry run is the way to see which listeners a phase covers before you act on it.
bondy.listener.suspend("normal", dry_run=true)The reserved admin listener
admin is a name, not a keyword, but it is reserved: the listener manager always includes one, either the operator's own listeners.admin.* block or its own default (tcp, port 18081, start_phase = early, services admin_api, wamp_ws, admin, metrics, bound to loopback). Declaring it overrides the default in place; an operator does not have to declare it at all. What cannot be done is disabling or removing it — a node that could lose its only administrable listener to a configuration mistake would be harder to recover than one that refuses that mistake outright.
A second, internal listener — a Unix domain socket at <platform_runtime_dir>/bondy_admin.sock — is injected unconditionally and is not configurable through bondy.conf at all. It exists so the node stays administrable even if every TCP listener fails to bind. Its socket file is the only access control it has — there is no peer address to filter on — so Bondy narrows that one file to mode 0600 after binding it, and refuses to serve on it if that fails. This applies to the internal socket alone: a uds listener an operator declares keeps the mode the process umask gives it, so a sidecar running under a different uid can reach it.
The socket lives under platform_runtime_dir, which is deliberately not platform_tmp_dir: it has to be on a filesystem that supports Unix domain sockets, and no other node can share it. The defaults satisfy both. A container image has to provide that directory: the node refuses to boot when the internal socket cannot bind, so an image built from a custom Dockerfile needs the runtime directory created and owned by the bondy user, the way the official images do.
Authenticating to the Admin API
On every listener that carries admin_api except the internal socket, each Admin API request needs an Authorization: Bearer access token issued by the master realm, com.leapsight.bondy. A request without a valid token gets 401. The check is the oauth2 scheme of an API Gateway specification, so the master realm's own configuration decides who can get and present a token:
- add
oauth2to the master realm'sauthmethods, and anoauth2source for the network your administrators connect from; - obtain tokens from
/oauth/tokenon the same listener, which also serves/oauth/revoke. See Protecting an HTTP API with OAuth2 for the grants.
A token from another realm is refused, even for a user of the same name. The token's user still needs grants on the master realm, as for any other procedure call.
A browser can use OIDC instead. A request without an Authorization header that carries the master realm's ticket cookie, bondy_ticket_com.leapsight.bondy, authenticates with it, checked as on an API Gateway path with the oidc scheme:
- add
cookieto the master realm'sauthmethods, and an OIDC provider to itsoidc_providers; - add a
cookiesource for the network your administrators connect from. An OIDC user without a local user record is matched only by sources assigned toall, because a source that names users requires them to exist; the provider'sauto_provisioncreates the record; - load an API Gateway specification bound to
com.leapsight.bondywhose security isoidcwith that provider: it serves the login, callback and logout endpoints that set the cookie; - a request other than
GET,HEADorOPTIONSmust also send the value of thebondy_csrf_com.leapsight.bondycookie inx-csrf-token.
The cookie's path is /, and without a cookie_domain in the provider's configuration it is sent only to the host that set it, on any port. Serve the login from the host the Admin API answers on, or set cookie_domain to cover both. When a request sends a bearer token as well, the token is the credential and the cookie is ignored.
Requests on the internal socket need no token. They run as the local operator, authid <admin_local>, and RBAC does not apply to them: only the node's Unix user and root can open the socket. Administration therefore stays possible from the node itself after every master-realm administrator is deleted or loses their grants. No other listener gets this treatment, including a uds listener an operator declares.
UDS listeners
A connection over a Unix domain socket has no network peer to log, embed in events, or match against an RBAC Source rule. Every listener that binds over uds — the internal admin socket and any operator-declared one — represents such a connection as loopback, 127.0.0.1, with port 0. A Source rule scoped to 127.0.0.1/32 therefore also matches a client arriving over a uds listener, which is consistent with what the rule already expresses: a Unix socket is reachable only by local processes, a subset of what a loopback TCP listener admits.
bridge_relay is not available over uds: a bridge relay is a network link between Bondy nodes, and there is no driver for one over a local socket. listeners.$name.transport = uds combined with listeners.$name.protocol = bridge_relay is refused at boot, naming the listener.
Bind address and IP version
Narrows the listener to one interface. Takes an address literal, v4 or v6 — it is parsed while the file is read, so a hostname is refused there and then.
A listener that sets no ip and serves the admin or metrics service binds loopback, 127.0.0.1. Every other listener without an ip binds the wildcard of its family. An explicit ip overrides the loopback rule, on any listener.
4 or 6; selects the socket family — the thing that decides whether Bondy opens an IPv4 or an IPv6 socket.
Apart from that loopback rule, neither key has a default, so a disagreement between them is always two explicit statements rather than one of them being supplied for you.
An address, where one is given, determines the family by itself. An address carries its own version and cannot be bound on a socket of the other one, so ip_version decides the family only for a listener that configures no address — there it chooses which wildcard is bound, 0.0.0.0 for 4 and :: for 6. Where both keys are set and disagree, the address wins and ip_version has no effect: ip = 127.0.0.1 with ip_version = 6 binds IPv4.
Two listeners may share a port when they bind different addresses. A socket is identified by its address and port together, so 10.0.0.1:443 and 10.0.0.2:443 are two sockets and Bondy starts both — one certificate per interface on a single port is the case this exists for. A listener that configures no ip binds the wildcard, which covers every address on its port and so excludes every other listener there; Bondy refuses that at startup, naming both listeners, rather than letting the second one fail its bind.
Addresses of different families count as overlapping whenever either is a wildcard, so :: and 127.0.0.1 on one port are refused together. Whether they truly collide depends on the host's bindv6only setting, and Bondy does not consult it — the refusal is deliberately the conservative answer.
Connection and socket tuning
The number of acceptor processes for the listener's socket.
The maximum number of concurrent connections on this listener.
The maximum length that the queue of pending connections can grow to. Unset, the OS default applies.
Enables periodic TCP-level keepalive transmission on a connected socket when no other data is exchanged.
Turns on TCP_NODELAY, so small amounts of data are sent immediately.
Sets SO_REUSEPORT on the listening socket.
The minimum size of the socket's kernel send buffer.
The minimum size of the socket's kernel receive buffer.
The size of the user-level software buffer used by the driver — not to be confused with sndbuf and recbuf, which are the kernel buffers. It is recommended to keep buffer >= max(sndbuf, recbuf) to avoid unnecessary copying; the driver raises it to that maximum automatically when sndbuf or recbuf are set.
How long a tls listener allows the TLS handshake to take.
When a raw-socket connection process hibernates to reclaim memory.
Overrides the value of the server header an HTTP listener sends.
bridge_relay listeners only: how long a connected peer router has to authenticate before the connection is dropped. Other protocols ignore it.
Proxy protocol and forwarded headers
These keys decide the source IP that Bondy logs, rate-limits by and matches against RBAC Source rules. When proxy_protocol is off, the source IP is the socket peer.
listeners.public_http.proxy_protocol = on
listeners.public_http.proxy_protocol.mode = strict
listeners.public_http.proxy_protocol.trusted_proxies = 10.0.0.0/8, 192.168.1.10/32The meaning depends on the listener's protocol:
- On a
wamp_rawsocketorbridge_relaylistener, Bondy reads a PROXY protocol header from each connection and takes the source address from it. Bondy waits up to 15 seconds for the header.trusted_proxiesis not consulted. - On an
httplistener, Bondy reads the forwarding headers instead, and believes them only when the socket peer is inside one of thetrusted_proxiesranges.
The mode decides what Bondy does when it cannot establish a source address. On an HTTP listener, that happens when the headers of a trusted peer yield no address. On a raw socket, it happens when the PROXY header is missing or malformed. relaxed falls back to the socket peer. strict refuses the connection or the request. The API Gateway answers a refused request with 403 and bondy.error.proxy_protocol_error.
A comma-separated list of CIDR ranges, e.g. 10.0.0.0/8, 172.16.0.0/12. It applies to http listeners only. The default is empty: no proxy is trusted, so with proxy_protocol = on and no trusted_proxies every peer is untrusted and the source IP is the socket peer. When an untrusted peer sends forwarding headers, Bondy ignores them and logs a warning. An IPv4 range never matches an IPv6 peer. When an entry is not a valid CIDR range, the node does not start and reports {invalid_trusted_proxies, Entries}, so a typo cannot leave a proxy untrusted without notice.
From a trusted peer, Bondy tries the headers in this order, and the first that yields an address wins:
Forwarded— thefor=values.X-Forwarded-For.X-Real-IP.
For the first two, Bondy reads the chain from the right and takes the first address that is not inside a trusted range. A client behind a trusted proxy therefore cannot shift its apparent source IP by prepending hops of its own. Bondy skips a chain entry that is not an IP address.
Keepalive and idle timeouts (raw-socket protocols)
idle_timeout and the ping.* block apply to wamp_rawsocket and bridge_relay listeners; an HTTP listener ignores them — a WebSocket connection's keepalive is configured under its carrier (listeners.$name.websocket.ping.*), and Cowboy owns the idle timeout for a plain request.
The reap deadline: how long a connection may be silent before Bondy closes it. Only data from the peer restarts this timer, and a pong counts as data, so Bondy never reaps a peer that answers probes.
Whether Bondy probes a silent connection with protocol-level PING messages. This affects server-initiated pings only; Bondy always answers a client's pings regardless.
How long a connection may be silent before Bondy probes it. This is a different and much shorter thing than the listener's idle_timeout, and keeping them apart is the point: a probe is only useful if it comes due well before the reap, and a keepalive whose interval equalled the reap deadline could neither hold a NAT binding open nor detect a dead peer any sooner than the reap already did.
How long a probe waits for its answer before counting as a failed attempt.
How many unanswered probes mean a dead peer. A peer that stops responding is dropped after ping.idle_timeout + max_attempts × ping.timeout. The transport does not change that judgement: tcp and tls listeners take the same defaults, and the WebSocket carrier's own ping defaults are the same numbers, so max_attempts means one thing across every protocol.
linger.timeout is in seconds
How long close blocks waiting for unsent data to be acknowledged, on a raw-socket or bridge-relay listener. It is the one duration in bondy.conf expressed in seconds rather than milliseconds, because the value becomes the OS socket option {linger, {true, N}} and that component is seconds. A value written as a bare integer also means seconds. A sub-second value rounds up to one second: {linger, {true, 0}} does not mean "linger briefly", it means abort the connection on close and discard whatever is unsent, so rounding down would silently turn a graceful close into a reset.
-1 disables lingering, which is the OS default behaviour — close returns immediately and the kernel finishes sending in the background. 0 requests the abort described above.
listeners.$name.http.linger.timeout is a different setting: it is Cowboy's, it governs how long the HTTP server waits before closing to avoid the TCP reset problem in RFC 7230 §6.6, and it is in milliseconds. The two are unrelated despite the near-identical spelling.
HTTP protocol options
For an HTTP listener, protocol-level settings — timeouts, header limits, the dynamic receive buffer — carry an http. prefix (listeners.$name.http.idle_timeout, not listeners.$name.idle_timeout), because the bare name already means something different on a raw socket and a setting whose meaning depended on a sibling protocol value would be worse than a longer one. They apply to everything the listener serves, whichever services are mounted on it.
A comma separated, priority ordered list of the HTTP protocol versions the listener offers; the default is 2, 1.1. On a TLS listener the order is the server's ALPN preference; HTTP/2 is served only to a client that negotiates h2 via ALPN (RFC 7540 requires that), so a client that negotiates http/1.1 or sends no ALPN is served HTTP/1.1 whenever 1.1 is listed. On a plaintext listener, listing 2 enables the HTTP/2 prior-knowledge and Upgrade paths. Set 1.1 alone to switch HTTP/2 off for a listener.
How long a connection with no incoming traffic is kept before Cowboy closes it. On a listener serving held streams the default changes — see Held streams.
Whether data sent by the server also resets the idle timer (received bytes always do). On a listener serving held streams the default changes — see Held streams.
How long to wait for a request to arrive on a keep-alive connection.
The number of packets requested from the socket at once.
The bounds between which Cowboy resizes a connection's socket receive buffer. Bondy checks these rules at boot:
- Set both keys or neither. One without the other refuses the boot.
- Each value is within 1KB..128KB, and
minis at mostmax. - Either key set to
0disables dynamic buffering for the listener, whatever the other key is.
Unset, Cowboy's own default applies: an adaptive buffer between 512 bytes and 128KB, unless the socket buffer size is set, in which case there is no dynamic buffering. A WebSocket connection uses the buffer of the listener it arrived on; there is no WebSocket-specific buffer setting.
Cowboy's linger timeout, in milliseconds — see the note under the raw-socket linger.timeout.
Maximum number of concurrent streams (in-flight requests) a single HTTP/2 client connection may have open. Bondy sets no default: unset, Cowboy advertises 100.
One HTTP/2 connection carries many requests. Anything that counts connections — max_connections, a connection-count alarm, a load balancer's connection metric — therefore undercounts the request load of an HTTP/2 client by up to a factor of max_concurrent_streams.
Cowboy also caps, per connection, the rate of received frames, of stream resets and of cancelled streams. Bondy neither sets nor exposes these caps, so Cowboy's defaults apply:
| Cap | Cowboy default |
|---|---|
| Received frames | 10000 per 10s |
| Stream resets | 10 per 10s |
| Cancelled streams | 500 per 10s |
The amount of HTTP/2 flow-control credit a new stream starts with.
Maximum length, in bytes, of the request authority (Host or :authority). Unset, Cowboy applies 255. Over the limit, HTTP/1.1 answers 414 and HTTP/2 resets the stream.
Maximum length of the Authorization header value, in bytes, independent of the general header-value limit — Authorization headers routinely carry bearer tokens (JWTs) larger than typical header values. Unset, max_header_value_length applies to it. Cowboy enforces it on HTTP/1.1 and answers 431.
Maximum size, in bytes, of the Cookie header value, independent of the general header-value limit. Unset, max_header_value_length applies to it. Cowboy enforces it on HTTP/1.1 and answers 431.
Maximum number of cookies parsed out of the Cookie header. The two cookie limits are not interchangeable: a single header of legal length can still hold thousands of tiny cookies, and it is the count that decides how much parsing one request can cost. Both matter on the endpoints that read cookies — the OIDC flow, the ticket and CSRF cookies, and the SSE and long-poll carriers. A request over max_cookies is answered with a 400.
One endpoint is an exception to the count limit. An API Gateway /oidc/verify endpoint treats a request over max_cookies as carrying no cookies at all, the same as a malformed Cookie header, and does not refuse it. Its credential can then come only from a bearer token or the x-bondy-ticket header, so an over-limit request without one is unauthenticated rather than rejected.
The maximum number of requests served on one keep-alive connection.
What to do with a response whose header values contain a CR or LF. error_terminate terminates the stream instead of sending the response, which prevents header injection and response splitting. ignore sends the response unchanged. Keep the default unless a legacy upstream behind the API Gateway requires relaying such headers.
Whether the sendfile syscall may be used.
Fixed request limits and validation
A query string or an application/x-www-form-urlencoded body carries at most 100 fields. A request with more is refused. The limit is Cowboy's default and Bondy does not expose it.
Bondy inherits these checks from Cowboy. They are not configurable:
- Fragments. An HTTP/1.1 request-target that contains a fragment (
#…) is rejected with400(RFC 9110 §4.1). - Authority and
Hostagree. An HTTP/1.1 request whose absolute-form authority differs from itsHostheader is rejected with400. An HTTP/2 request whosehostheader differs from its:authorityhas its stream reset (RFC 9113 §8.3.1). - Pipelining. Cowboy reads pipelined HTTP/1.1 requests one at a time: it does not parse the next request on a connection until the current stream has finished.
Held streams and the connection idle timeout
On a listener whose services include wamp_sse, wamp_longpoll or mcp, the defaults for http.idle_timeout and http.reset_idle_timeout_on_send change: the idle timeout is taken from the largest held-stream idle_timeout in play (10m if unstated) and reset-on-send is switched on, so a stream that is sending — events, keepalives, poll replies — keeps its connection alive, and a fully quiet one is closed after the carrier's idle timeout. This applies to every service sharing that listener: idle keep-alive API connections on a mixed listener are retained for the same window. An explicit http.idle_timeout or http.reset_idle_timeout_on_send on the listener overrides both. The behaviour is identical over HTTP/1.1 and HTTP/2.
CORS
The cors.* block is per-listener and meaningful only for protocol = http. A listener that sets none of it does not come up closed: the settings it did not state are taken from Bondy's own defaults — enabled = on, allowed_origins = * — so declaring a listener with no cors.* emits wildcard CORS, not none. The defaults fill in per key: restricting allowed_origins leaves allowed_methods and allowed_headers at their defaults unless those are stated too. There is no global block to inherit from, so an HTTPS listener that has to restrict CORS origins to an allowlist needs that allowlist stated here; without it the listener is open to any origin.
Security headers
Per-listener, HTTP only; documented in detail in the HTTP Security Headers reference. Defaults fill in per key: unset, the listener sends SAMEORIGIN and nosniff, and a TLS listener additionally sends HSTS.
The all-or-nothing switch: off drops every security header.
To drop one security header, give it the value off.
listeners.public_http.security_headers.hsts = offThat suppresses just that header and leaves the rest, including the default HSTS a TLS listener would otherwise send. off is the only word treated this way — every other value is the header's content, so frame_options = office sends office. Note there is no "empty value" spelling: security_headers.hsts = with nothing after it is a syntax error in bondy.conf, not an empty setting.
Rate limiting
Per-listener budgets for the same token-bucket classes as the node-wide security.rate_limit.* keys. A request is admitted only when the node scope, this listener's scope and — where the request addresses one — the realm's own budgets all admit it, so a listener budget can only narrow what the node allows. The model is described in Understanding Load Regulation and Rate Limiting.
These keys have no defaults: a class block's presence enables that class's budget on this listener (independently of the node-scope master switch), and each budget requires both its rate (tokens per second) and capacity (burst size). The enabled key exists to park a configured budget without deleting its numbers. The checks are cheap enough to leave enabled everywhere — one lock-free atomic operation per configured scope per message, with no measurable effect on throughput or latency; see Checking is cheap — refusing is not.
listeners.public_ws.rate_limit.connection.rate = 30
listeners.public_ws.rate_limit.connection.capacity = 60Maximum sustained new-connection rate per source IP on this listener, in connections per second.
Maximum sustained WAMP HELLO rate per source IP on this listener, per second.
Maximum sustained authentication-attempt rate per source IP on this listener, per second.
Maximum sustained HTTP request rate per source IP on this listener, in requests per second (requests, not connections). Throttled requests answer 429 with a retry-after header.
Maximum sustained WAMP message rate (CALL/PUBLISH/SUBSCRIBE/REGISTER) per session established through this listener, in messages per second. The budget is resolved once at session open, like the node-scope message class.
Carrier settings
websocket.*, sse.*, longpoll.* and mcp.* configure the connections a listener serves — one connection style each; the listener's HTTP itself is the http.* block above. They belong to the listener the connection arrived on: setting one on listeners.public_http says nothing about listeners.admin, and there is no global block covering all listeners at once. A setting stated on the listener wins key by key, not block by block: a listener that sets only websocket.ping.idle_timeout keeps the defaults for ping.enabled, ping.timeout and ping.max_attempts.
The mcp.* carrier keys are documented in the MCP Gateway Configuration Reference.
WebSocket
Whether Bondy probes a silent WebSocket connection with PING control frames. Server-initiated pings only; Bondy always answers a client's pings regardless. Note that web browsers typically do not initiate WebSocket pings and kill quiet connections, which is why this is on by default.
How long the connection may be silent before Bondy probes it.
How long a probe waits for its answer before counting as a failed attempt.
How many unanswered probes mean a dead peer.
Drops the connection after this period of inactivity.
Maximum frame size accepted. The connection is closed when a client attempts to send a frame over this limit; for fragmented frames it applies to the reconstituted frame.
When the connection process hibernates: idle hibernates between periods of activity, always after every message, never not at all.
Whether WebSocket compression (the permessage-deflate extension) is negotiated with supporting clients. The deflate.* block only takes effect when this is on.
Compression level: 0 gives no compression, 1 best speed, 9 best compression.
How much memory is allocated for the internal compression state: 1 uses minimum memory but is slow and reduces the compression ratio, 9 uses maximum memory for optimal speed.
Tunes the compression algorithm: default for normal data, filtered for data produced by a filter or predictor, huffman_only to force Huffman encoding only, rle to limit match distances to one (run-length encoding). Strategy affects only the compression ratio, not the correctness of the output.
Using no_takeover can severely limit the usefulness of compression.
The base-two logarithm of the compression window size. Larger values give better compression at the expense of memory.
SSE
Whether the SSE stream carries periodic keepalive comments.
How often the stream sends a : keepalive comment while sse.ping.enabled is on.
How long a fully quiet SSE stream is kept. Feeds the listener's connection idle-timeout default — see Held streams.
Long-poll
How long a poll request is held open waiting for messages. The value has to stay strictly below longpoll.idle_timeout, or the connection can be torn down for inactivity before the long-poll reply is sent.
How long an idle connection between polls is kept. Feeds the listener's connection idle-timeout default — see Held streams.
TLS material
A tls-transport listener needs a certificate and key, and is checked for them at boot. The check applies only to a listener that will start: enabled = off skips it, so you can declare a TLS listener and provision its certificate later. The check is deferred, not dropped — turning it on runs it, and a listener that is still missing its certificate is refused then. That refusal fails the whole boot, not just that listener: the inventory is resolved as a unit, so one unusable entry stops every other listener starting too.
The tls.* block is the only place a listener's certificate is read from — the reserved admin listener can be given one here directly, like any other.
The PEM certificate (or chain) file.
The PEM private key file.
The CA certificate file, needed when requesting client certificates.
Comma-separated TLS protocol versions, of 1.2 and 1.3.
For mTLS, verify_peer on its own only requests a client certificate — a client presenting none still connects. Requiring one takes fail_if_no_peer_cert too:
listeners.public_wamp_tls.tls.verify = verify_peer
listeners.public_wamp_tls.tls.cacertfile = /path/to/cacert.pem
listeners.public_wamp_tls.tls.fail_if_no_peer_cert = onBondy sets neither supported_groups nor signature_algs on a listener, so the key-exchange groups and signature algorithms on offer are the OTP ssl defaults for the negotiated TLS version.
Boot-time validation
Bondy resolves the listener inventory as a unit at boot. It logs the first invalid entry as Invalid listener configuration, with reason {invalid_listener, Name, Reason}, and the node does not start: one bad listener refuses the whole node, not just itself. Reason is one of:
| Reason | Cause |
|---|---|
{missing, transport}, {missing, protocol} | The key is absent. |
{unknown_transport, T} | transport is not tcp, tls or uds. |
{unknown_protocol, P} | protocol is not http, wamp_rawsocket or bridge_relay. |
{unsupported_combination, uds, bridge_relay} | A bridge relay over a Unix domain socket. |
{missing, port}, {missing, path} | No bind target: path for uds, port otherwise. |
duplicate_name | Two listeners share a name. |
{port_in_use_by, Other}, {path_in_use_by, Other} | The bind target overlaps the bind target of listener Other. See Bind address and IP version. |
{invalid_ip, Ip}, {unresolvable_ip, Ip, Reason} | ip is not a valid address. Only an inventory in sys.config, which can name a host, reaches the second. |
{invalid_trusted_proxies, Entries} | proxy_protocol.trusted_proxies holds entries that are not valid CIDR ranges. |
{missing, services} | An http listener with no services, or an empty list. |
{services_not_supported, P} | services on a wamp_rawsocket or bridge_relay listener. |
{unknown_service, S} | S is not a service this node knows. |
{unknown_carrier, Carrier, Service} | An extension registered Service with a carrier that no module serves. |
{incompatible_services, mcp, admin_api} | One listener declares both. |
{missing, [tls, certfile]}, {missing, [tls, keyfile]} | A tls listener without its certificate or key. |
{tls_not_supported, T} | A tls.* key on a tcp or uds listener. |
reserved_name | The name is admin_local, which the node injects itself. |
reserved_cannot_be_disabled | listeners.admin.enabled = off. |
{invalid_ping_enabled, Label, V}, {incomplete_ping, Label, Keys} | A malformed ping block. Only sys.config can express one; bondy.conf cannot. |
A listener with enabled = off skips the TLS checks, as TLS material describes. Every other check applies to a disabled listener too.
Migrating from the pre-1.0 keys
The per-scheme keys that configured Bondy's fixed listeners have been removed. Nothing reads them: a file that still sets them loses every one of those settings silently, because an unknown key is dropped rather than refused. The global carrier keys wamp.websocket.*, wamp.sse.* and wamp.longpoll.* are removed for the same reason; a single global key covered every listener at once, so moving one means choosing which listeners it applies to — restate it under listeners.<name>.<carrier>.* for each listener that serves the carrier, or drop it if it only restated the default (the defaults are unchanged).
listeners.admin.* listeners.admin_api_https.* listeners.api_gateway_http.* listeners.api_gateway_https.* listeners.wamp_tcp.* listeners.wamp_tls.* listeners.bridge_relay_tcp.* listeners.bridge_relay_tls.* listeners.$name.websocket.* listeners.$name.sse.* listeners.$name.longpoll.* Only the first rename is forced: admin is the reserved name for the administrable listener — the manager always provides one, so declaring the same listener under any other name gives you two listeners competing for one port. The other names are the ones scripts/migrate_conf.escript (shipped with the Bondy source) rewrites to; it reports every affected key and listener, and also catches the two mistakes a hand migration makes:
- A listener you do not declare does not exist. A file with no
listeners.*key at all starts the three built-in defaults (admin,api_gateway_http,wamp_tcp) and nothing else — every TLS listener and every bridge-relay listener is gone. - Renaming the option keys is not enough. A
listeners.<name>.*block that carries options but notransport,protocoland bind target is refused at boot with{invalid_listener, <name>, {missing, transport}}, which aborts the whole node rather than skipping that listener.
The tails move too, and three groups do not keep their spelling:
- TLS material (
certfile,keyfile,cacertfile,versions,verify,fail_if_no_peer_cert) moves underlisteners.<name>.tls.*. - Cowboy protocol options on an HTTP listener (
idle_timeout,max_headers,active_n,linger.timeoutand the rest) move underlisteners.<name>.http.*. Note thatidle_timeoutandlinger.timeoutexist in both places: on an HTTP listener they are Cowboy's, on a raw-socket or bridge-relay listener they are the listener's own and stay at the top level. bridge.listener.<t>alone was that listener'senabledflag, andbridge.listener.<t>.pingalone wasping.enabled.
Two keys have no destination: bridge.listener.{tcp,tls}.max_frame_size was never read by anything, and admin_api.*.dynamic_buffer.* was the internal name of buffer.min/buffer.max.
Some keys change name as they move, and some have no destination. scripts/migrate_conf.escript applies the same table:
| Key | Destination |
|---|---|
wamp.websocket.ping.interval | listeners.<name>.websocket.ping.idle_timeout |
wamp.websocket.ping.max_retries | listeners.<name>.websocket.ping.max_attempts |
wamp.websocket.buffer.min, .max | None. A WebSocket connection uses the buffer of its listener; set listeners.<name>.http.buffer.min and .max. |
wamp.sse.reset_idle_timeout_on_send, wamp.longpoll.reset_idle_timeout_on_send | None. The working control is listeners.<name>.http.reset_idle_timeout_on_send, which covers every service on the listener. |
wamp.transport_queue.transport_ttl | wamp.http_transport.idle_timeout |
wamp.transport_queue.overflow_strategy | None. Eviction of the oldest entry is the only policy. |
wamp.transport_queue.* (the rest) | wamp.http_transport.queue.* |
On the keys of a raw-socket or bridge-relay listener, ping.interval becomes ping.idle_timeout and ping.max_retries becomes ping.max_attempts.
listeners.$name.ip takes a literal address, while the removed *.ip keys accepted any resolvable hostname. An inventory supplied through sys.config is still resolved by name.