Skip to content

Daemon configuration

By default, the Peppy daemon reads one global configuration file, ~/.peppy/conf/peppy_config.json5. It sets the daemon’s core-node name, selects either a Peppy-managed or operator-run Zenoh router, controls the managed router’s messaging behavior, and defines the grace periods that govern node lifecycle. The daemon applies it to its own core-node session and to every node it spawns. The same file also records the backend resource-server URL the peppy platform login / whoami / logout commands talk to; that block is read by the CLI, not the daemon. PEPPY_CONFIG can supply a read-only alternative source.

In the normal on-disk flow, you never need to create or migrate this file by hand:

  • First load. If the file does not exist, Peppy creates it with every setting at its default value, annotated with explanatory comments.
  • Missing settings. If the file exists but omits settings (typically a file written by an older Peppy, before a newer knob existed), Peppy detects every omission and appends each setting it can safely place, together with its default value and comments. Your own values, comments, formatting, and any unrecognized top-level keys are otherwise preserved exactly as you wrote them; when appending defaults, completion may add a structural separator comma after the prior final entry. Peppy logs each setting it added. An unusual JSON5 spelling or layout, such as an escaped section key that the text scanner cannot safely pair with its block, produces a warning naming every setting that could not be inserted; the daemon continues with those defaults in memory. Fields required inside a non-default variant are not invented: if you select zenoh.external, you must supply zenoh.external.endpoint.
  • Malformed file. If the file cannot be parsed, or a value is out of range, Peppy refuses to continue and reports the error instead of silently falling back to defaults. A file that fails to load is never modified.

In a normally spelled section, a defaulted setting you delete from the file therefore comes back with its default the next time Peppy loads the configuration. To change one, edit its value instead of removing it. If Peppy warns that an unusual spelling or layout prevented insertion, add the named setting manually if you want it written; the daemon already uses its default in memory. Removing zenoh.external.endpoint from an external block instead makes the file invalid and leaves it untouched.

Set PEPPY_CONFIG to inject the complete daemon configuration at launch. A non-empty value has two forms: a path to a JSON5 config file, or an inline JSON5 document. Peppy expands ~ and ~/ in path form, and resolves relative paths from the command’s current working directory, which is the daemon’s working directory for peppy service serve.

Classification always tries the value as a file first. If the file can be read, file form is committed, so a later parse, validation, or completeness error does not fall back to interpreting the path text as inline JSON5. Only a file read or path-expansion failure triggers the inline parse. If both attempts fail, the error reports both reasons. An empty value is treated as unset and uses the normal file flow; whitespace alone is not empty and fails if it is neither a readable path nor valid inline JSON5.

An override is read-only. Peppy ignores $PEPPY_HOME/conf/peppy_config.json5 and never creates, completes, or rewrites either that file or the override source. The override must parse, pass validation, and explicitly spell out every setting the running release defines. Missing settings are a startup error that names their paths and explains that override sources are never completed with defaults. This differs from the normal on-disk flow, which appends safe defaults and warns when an unusual source spelling prevents an insertion.

The override applies to peppy service serve and to every auth command that reads daemon configuration: peppy platform login, logout, and whoami. PEPPY_HOME still controls credentials, repositories, and all other Peppy state. Edits to a file-form override take effect on the daemon’s next restart, just like edits to the default file.

Both examples below use the same complete config file. The second passes its full contents inline, including any leading JSON5 comments:

Terminal window
PEPPY_CONFIG=./deploy/peppy_config.json5 peppy service serve
PEPPY_CONFIG="$(cat ./deploy/peppy_config.json5)" peppy service serve
// Daemon settings are read once at startup, so edits to them take effect only
// after restart. resource_servers is refreshed by each CLI authentication command.
{
// Fixed name for this daemon's core node, or null to derive a
// machine-specific default (cn-...). Names must be unique across all
// daemons reachable over the same router/federation: a daemon whose name is
// already in use refuses to boot. At most 63 characters from
// "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-".
// `peppy service serve --core-node-name` overrides this for one run.
core_node_name: null,
// The zenoh messaging transport. Configure exactly one of two blocks:
// managed: peppy starts, monitors, restarts, and stops its bundled
// zenohd router, and every knob inside `managed` applies.
// external: you run the router and peppy only dials it; peppy never
// starts, reconfigures, restarts, stops, or federates it,
// local nodes always relay through it, and the managed knobs
// do not exist. Spelled, in place of `managed`, as:
// external: { endpoint: "tcp/<host>:<port>" },
zenoh: {
// Settings peppy can only honor because it owns the router process. Replace
// this whole block with `external` to use your own router.
managed: {
// How the nodes on this machine exchange data with each other. Traffic to
// and from other machines always relays through the local zenohd router and
// its federation, regardless of this setting (node sessions only accept
// direct links over loopback).
// "peer" - Zenoh peer sessions with gossip: local nodes form direct
// peer-to-peer links and data stops relaying through the router.
// "router" - gossip off: all traffic relays through the central zenohd
// router.
// Container nodes in a separate network namespace (Lima on macOS) always use
// the router path regardless of this setting.
local_nodes_topology: "peer",
// Subscriber channel buffer sizes (number of in-flight messages) per QoS
// tier, used by local node sessions in both managed topologies. They matter
// most in peer mode, where no router relay buffers between a publisher and
// a subscriber. Defaults match peppy's built-in behavior; only edit to tune
// backpressure.
subscriber_buffers: {
standard_buffer_size: 128,
high_throughput_buffer_size: 1024,
},
// Per-user zenoh-router federation: how the daemon links its local router to
// your private cloud router. Only tuned to bound a slow/unreachable backend
// during the federation step.
federation: {
// Seconds the daemon spends resolving your per-user cloud router before
// giving up for this attempt (it retries in the background). Bounds the
// federation done at startup and on each `peppy platform login`/`logout`;
// minimum 1. If the backend is unreachable within this window the daemon
// stays standalone rather than blocking.
connect_timeout_secs: 30,
},
},
},
lifecycle: {
// Node lifecycle knobs. `daemon_grace_secs` is the grace period a spawned node
// waits, after the daemon's heartbeat goes silent, before shutting itself down
// to avoid orphaning.
daemon_grace_secs: 180,
// How long a clean shutdown (ctrl+C / `systemctl stop`) and `peppy node
// stop` wait for a node to exit cooperatively before force-killing its
// process group. Seconds; minimum 1. A robot node uses this window to park
// actuators and release hardware before it is killed.
shutdown_grace_secs: 5,
},
// Backend resource-server URL the `peppy platform login` / `whoami` / `logout`
// commands talk to. Baked in at compile time (the dev backend in debug
// builds, prod in release); --api-url / PEPPY_API_URL override it at runtime.
resource_servers: {
api: "https://api.peppy.bot",
},
}

core_node_name: the daemon’s core-node name

Section titled “core_node_name: the daemon’s core-node name”

core_node_name fixes the name of this daemon’s core node. Leave it null (the default) and the daemon derives a stable, machine-specific name of the form cn-{adjective}-{surname}; set a string to pin an explicit one.

Core-node names must be unique across every daemon reachable over the same router or federation. Each daemon advertises an ephemeral presence token; on boot it refuses to start if that name is already live, and while running it logs a rate-limited collision alarm if another daemon later claims the same name. If two daemons end up sharing a name, give one of them a unique core_node_name (or pass peppy service serve --core-node-name <name> for a single run) and restart it. This most commonly matters when logging in federates several machines together, or when cloning a disk image reuses another machine’s derived name.

The value must be non-empty, at most 63 characters, and use only the node-name character set (start with a letter; letters, digits, _, -). An invalid value stops the daemon at startup with an error instead of failing later when the core node boots. peppy service serve --core-node-name <name> overrides the config for one run (the flag wins over the file); if both are absent, the derived default is used.

The zenoh section holds exactly one of managed or external. Block presence selects the mode. managed is the default: omitting zenoh or writing zenoh: {} loads all managed defaults, and normal file completion materializes the explicit managed block.

Use managed when Peppy should start, monitor, restart, and stop its bundled zenohd:

zenoh: {
managed: {
local_nodes_topology: "peer",
subscriber_buffers: {
standard_buffer_size: 128,
high_throughput_buffer_size: 1024,
},
federation: {
connect_timeout_secs: 30,
},
},
},

Use external when Peppy should dial a router that you run:

zenoh: {
external: {
endpoint: "tcp/127.0.0.1:7448",
},
},

The managed knobs exist only because Peppy controls that router process. Under external, local_nodes_topology, subscriber_buffers, and federation are not part of the document, and writing them is an error. Local nodes always relay through the external router, local subscriber queues use Peppy’s built-in capacity defaults, and router federation belongs entirely to the operator.

zenoh.managed.local_nodes_topology: local messaging topology

Section titled “zenoh.managed.local_nodes_topology: local messaging topology”

zenoh.managed.local_nodes_topology selects how the nodes on this machine exchange data with each other:

  • "peer" (default): nodes run Zenoh peer sessions with gossip discovery enabled. After discovery, nodes form direct peer-to-peer links and data stops relaying through the central router, which removes a network hop from every message.
  • "router": gossip is off and all traffic relays through the central zenohd router. Use this when direct node-to-node connectivity is unreliable or undesirable, or to simplify debugging by funneling all traffic through one process.

The setting is local by construction, which is why it carries the local_nodes_ prefix: a node session only accepts direct links on a loopback listener, so gossip can only ever form direct links between sessions co-located on this machine. Traffic to and from nodes on other machines always relays through the local router and its federation, whichever topology you pick.

Container nodes that live in a separate network namespace (such as the Lima VM peppy uses on macOS) always take the router path, regardless of this setting: gossip cannot establish direct links across the namespace boundary.

zenoh.managed.subscriber_buffers: subscriber buffer sizes

Section titled “zenoh.managed.subscriber_buffers: subscriber buffer sizes”

Each Peppy subscriber buffers in-flight messages in a bounded process-local queue. The zenoh.managed.subscriber_buffers block sets that queue’s capacity (number of messages, not bytes) per QoS tier:

  • standard_buffer_size (default 128): the local queue for topics on the standard QoS tier, which most topics use. The same capacity also sizes the queues behind service requests and replies.
  • high_throughput_buffer_size (default 1024): the local queue for topics on the high-throughput QoS tier, such as sensor-data streams, where short bursts well above the average rate are normal.

These are local Peppy queue capacities, not Zenoh router buffers, and they apply under both managed local topologies. They matter most in the peer topology: with nodes peering directly there is no router relay between a publisher and a subscriber, so the local queue is all that absorbs a burst.

When a subscriber falls behind and its local queue fills up, what happens next depends on the topic’s QoS: topics published with a reliable profile block delivery so backpressure propagates to the publisher, while best-effort topics (including sensor-data streams on the high-throughput tier) drop messages instead. Raise a capacity to absorb longer bursts at the cost of memory and worst-case latency; lower it to surface backpressure, or message loss, sooner. The defaults match Peppy’s built-in behavior, so you only need to touch this block to tune backpressure.

Both values must be greater than 0; the daemon rejects a zero buffer size at startup.

zenoh.managed.federation: cloud-router timeout

Section titled “zenoh.managed.federation: cloud-router timeout”

When you are logged in under zenoh.managed, the daemon links (“federates”) its local messaging router to your workspace’s private cloud router so that sessions using the same workspace namespace can interoperate across the federation (see Platform). The namespace is routing context only, not proof of membership, a tenant-isolation boundary, or an authorization policy. Resolving that cloud router involves a backend round-trip, and this block bounds it.

  • connect_timeout_secs (default 30, minimum 1): how long the daemon spends resolving the cloud router (once at startup, and again each time peppy platform login / logout pokes the daemon) before giving up for that attempt. If the backend is unreachable within the window, the daemon leaves its router standalone and retries federation in the background rather than blocking startup. A logged-out daemon never federates, so this timeout does not apply to it.

zenoh.external: use an operator-run router

Section titled “zenoh.external: use an operator-run router”

To use a router you start yourself, configure zenoh.external with the Zenoh endpoint Peppy should dial:

zenoh: {
external: {
endpoint: "tcp/127.0.0.1:7448",
},
},

endpoint is a dial address, not a listener bind address. It must use tcp/<host>:<port> with a hostname, an IPv4 address, or a bracketed IPv6 address, for example tcp/router.internal:7448 or tcp/[::1]:7448. The port is required and must be from 1 through 65535. Listener wildcards such as 0.0.0.0, [::], and * are rejected because Peppy cannot connect to them. Metadata/config suffixes (?… or #…) and non-TCP transports are not currently supported.

Peppy propagates this host and port to its CLI sessions and spawned nodes, so the address must be reachable from every environment that runs a node. For Lima containers on macOS, Peppy rewrites a host-local address such as 127.0.0.1 to the VM’s host gateway; a genuinely remote hostname or IP address is preserved.

Start the external router before Peppy. Its listener may bind a wildcard even though Peppy’s dial endpoint must be connectable. For a router on the same machine, for example:

Terminal window
zenohd -l tcp/0.0.0.0:7448

At startup, Peppy requires a responsive Zenoh router at the configured endpoint. Nothing listening is a startup error; a non-Zenoh service at the address is a distinct startup error. Peppy observes a responsive router’s health but never kills it on daemon exit, restarts it after a failure, or changes its configuration to federate it to the cloud router. The operator controls the router’s lifecycle and federation. If it becomes unresponsive while the daemon is running, the watchdog logs a prominent warning but leaves it alone; sessions reconnect automatically after the operator restores it.

peppy platform login and logout leave external federation untouched and print a note instead of poking the daemon. They do not restart the daemon; a sign-in change reaches its sessions on the next manual daemon restart. PEPPY_MESSAGING_PORT configures the listener for Peppy’s managed router, while external mode uses the port written in zenoh.external.endpoint.

The lifecycle block tunes the two windows Peppy uses to guarantee that no node outlives the daemon. The full mechanics are described in Daemon shutdown and orphan prevention.

  • daemon_grace_secs (default 180, minimum 30): every spawned node runs a watchdog that listens for the daemon’s periodic heartbeat (published every 5 seconds). If the heartbeat goes silent for this many seconds, the node shuts itself down rather than lingering as an orphan. This only governs unclean daemon death (crash, OOM, SIGKILL); a clean shutdown does not wait for it. The minimum exists so a brief daemon blip or a quick restart never trips every node’s watchdog.
  • shutdown_grace_secs (default 5, minimum 1): the node’s cooperative-cleanup budget. A clean daemon shutdown (Ctrl+C, systemctl stop) and peppy node stop wait this window, plus a fixed allowance for the node’s runtime to finish tearing down (in Rust, the async runtime dropping; in Python, the event-loop join and interpreter finalize), before force-killing its process group. Raise it for nodes that need longer to park actuators or release hardware before dying; the force-kill deadline rises with it.

This block holds the platform-backend base URL the CLI auth commands talk to. The daemon ignores it; the peppy platform login / whoami / logout commands read it. See Platform for the full login flow.

  • api (default https://api.peppy.bot in release builds, http://127.0.0.1:3000 in debug): the backend the auth commands talk to. There is no dev/prod selection at runtime; the file stores exactly the build’s backend.

The URL is resolved in precedence order: --api-url, then PEPPY_API_URL, then api here. An empty block falls back to the build’s default backend. Plain http is accepted only for local backends (loopback / *.localhost); any other host must be https, validated when the command runs.

SettingDefaultConstraintEffect
core_node_namenull (derived cn-...)Non-empty, ≤ 63 chars, node-name charsetFixed name for this daemon’s core node; must be unique across all daemons on the same router/federation or the daemon refuses to boot. Overridable per run with --core-node-name.
zenoh.managed.local_nodes_topology"peer""peer" or "router"Topology of this machine’s nodes: direct peer links with gossip, or relay everything through the central router. Cross-machine traffic always goes through the routers.
zenoh.managed.subscriber_buffers.standard_buffer_size128> 0Local Peppy subscriber queue capacity (messages) for standard-QoS topics; also sizes service request/reply queues.
zenoh.managed.subscriber_buffers.high_throughput_buffer_size1024> 0Local Peppy subscriber queue capacity (messages) for high-throughput-QoS topics such as sensor-data streams.
zenoh.managed.federation.connect_timeout_secs30>= 1Seconds the daemon spends resolving your workspace’s cloud router (at startup and on each auth login/logout) before falling back to a standalone router.
zenoh.external.endpointNo defaultRequired inside external; tcp/<dial-host>:<port>; no wildcard hostNetwork endpoint of the operator-run router. Peppy dials it without changing its federation or managing its lifecycle.
lifecycle.daemon_grace_secs180>= 30Seconds without a daemon heartbeat before a spawned node self-terminates (unclean daemon death only).
lifecycle.shutdown_grace_secs5>= 1Seconds a clean shutdown and peppy node stop wait for cooperative exit (plus a fixed runtime-teardown allowance) before force-killing.
resource_servers.apihttps://api.peppy.bot (release), http://127.0.0.1:3000 (debug)https, or http for local hostsBackend the CLI auth commands talk to. Read by the CLI, not the daemon.

Both Zenoh variants at once, a value outside its constraint, an unsupported zenoh.external.endpoint, an unknown zenoh.managed.local_nodes_topology value, an unknown key inside zenoh or either variant, or a syntax error all stop the operation that loaded the configuration with an error pointing at the problem. An out-of-range value names the offending field, and a parse error reports the bad value or its position, so a typo can never silently revert your stack to defaults. Unknown top-level keys remain accepted and preserved.