Stdio JSON-RPC Bridging for Cross-Process IPC Without Daemon Bloat

Living Document Notice
Published 2026-09-19. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.

Stdio JSON-RPC Bridging for Cross-Process IPC Without Daemon Bloat

Summary

Local developer tools that spawn long-running HTTP background daemons introduce operational hazards: orphaned server processes, TCP port contention, cross-site script request forgery vulnerabilities, and lingering background battery drain. Bosun PKM rejects long-lived background HTTP listeners in favor of transient child-process execution communicating over line-delimited standard I/O (stdio) JSON-RPC 2.0. This architecture guarantees automatic lifecycle synchronization and sub-millisecond query execution without daemon sprawl.

The Flaws of Localhost HTTP Daemons

Many desktop note applications run background HTTP or WebSocket servers bound to 127.0.0.1. In practice, local HTTP daemons present persistent engineering liabilities:

Localhost HTTP Daemon Pitfalls:
1. Port Clashes:       Port 8080/3000 collision with user dev servers
2. Security Perimeters: Malicious websites issuing localhost CORS/CSRF requests
3. Zombie Daemons:     Parent crashes leave rogue background processes holding file locks
4. High Idle Overhead: Persistent TCP sockets, event loops, and memory bloat (>50 MB)

By eliminating long-running network listeners, the toolchain operates without requiring listening TCP sockets or background port assignment registries.

The Stdio JSON-RPC 2.0 Protocol Frame

Communication between client interfaces (CLI, editor extensions) and the core Bosun engine flows through standard input (stdin) and standard output (stdout). Each message is a newline-delimited JSON-RPC 2.0 payload:

{"jsonrpc":"2.0","id":1,"method":"notes.get_backlinks","params":{"path":"engineering/storage.md"}}

The response follows the standard JSON-RPC 2.0 schema:

{"jsonrpc":"2.0","id":1,"result":{"backlinks":["architecture/manifesto.md","runbooks/ops.md"]}}

Strict protocol guardrails:

  • Maximum message frame: 64 KB per line (prevents memory exhaustion).
  • Format: strictly single-line newline-delimited UTF-8 (no multiline breaks inside JSON strings).
  • Diagnostics: stderr is reserved exclusively for unformatted human-readable logs and crash stack traces.

Child-Process Adapter Implementation

The client spawns the engine as a direct unprivileged subprocess. When the parent process exits or terminates, the operating system kernel closes the IPC pipe, terminating the child engine immediately:

import json
import subprocess
from typing import Any

class EngineRpcClient:
    def __init__(self, engine_binary: str):
        self.process = subprocess.Popen(
            [engine_binary, "--stdio-rpc"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1,  # Line-buffered
        )
        self._request_id = 0

    def call(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
        self._request_id += 1
        req = {
            "jsonrpc": "2.0",
            "id": self._request_id,
            "method": method,
            "params": params,
        }
        serialized = json.dumps(req, separators=(",", ":"))
        if len(serialized) > 65536:
            raise ValueError("RPC payload exceeds maximum 64 KB message limit")

        self.process.stdin.write(serialized + "\n")
        self.process.stdin.flush()

        response_line = self.process.stdout.readline()
        if not response_line:
            raise RuntimeError("Engine subprocess closed pipe unexpectedly")

        res = json.loads(response_line)
        if "error" in res:
            raise RuntimeError(f"RPC Error [{res['error']['code']}]: {res['error']['message']}")

        return res["result"]

    def close(self):
        self.process.stdin.close()
        self.process.terminate()
        self.process.wait()

Benchmark: Stdio vs Localhost HTTP

Evaluating request/response round-trip latency across 10,000 queries on Linux 6.8:

Transport Layer Round-Trip Latency (p50) Round-Trip Latency (p99) Idle Resident Memory (RSS) Lingering Zombie Risk
Localhost HTTP (TCP 127.0.0.1) 1.84 ms 6.20 ms 48.2 MB (Server daemon) High (Requires process reaper)
Unix Domain Socket (UDS) 0.52 ms 1.95 ms 24.5 MB (Socket daemon) Moderate (Dangling socket files)
Stdio JSON-RPC Subprocess 0.28 ms 0.88 ms 0.0 MB (Zero idle memory) Zero (Kernel cleans on pipe close)

Stdio JSON-RPC delivers the fastest IPC latency, eliminates idle background RAM consumption, and ensures leak-free process lifecycles.

← Back to Bosun PKM Tools Blog