pyrpc dev does two jobs: it keeps TypeScript types in sync, and it runs your server. Sometimes your server is already running, you started it yourself, or you’re on a second terminal and you only want the type watcher. How does dev know not to start a second uvicorn?
One HTTP probe
The answer is a single request with a short timeout:
def _server_is_running(host: str, port: int) -> bool:
try:
import httpx
resp = httpx.get(f"http://{host}:{port}/rpc", timeout=1.0)
return resp.status_code < 500
except Exception:
return FalseGET /rpc is the introspection endpoint that every pyRPC adapter exposes, so a successful response is a reliable “yes, a pyRPC server is alive here”. The< 500 check treats any server-side response, even a 404 from a non-pyRPC app on that port, as “something is listening, don’t bind the port again”. The except Exception blanket covers connection refused, timeouts, DNS failures, and anything else; any of those means nothing usable is there.
Why a probe, not a bind test
The obvious alternative, try to bind the port and see if it fails, has a real flaw: binding is not the same as running your app. A port can be occupied by an unrelated process, or your OS can briefly refuse a bind for reasons that have nothing to do with your server. The HTTP probe tests the property we actually care about: is the pyRPC API reachable at this address? A port can also be free while your server is mid-restart (uvicorn’s reloader closes the socket between processes), and a probe handles that transient state gracefully too.
The 1-second timeout keeps the probe from stalling startup on a dead or slow network path. A single retry is deliberately not performed, if the first probe fails, starting uvicorn is the safe fallback either way.
Attach mode
When the probe succeeds, dev prints a status line and skips uvicorn entirely:
if _server_is_running(host, port):
console.print(
f" [dim]○[/dim] server already running at "
f"http://{host}:{port}/rpc, skipping uvicorn"
)
else:
server_proc = _start_uvicorn(module)
server_managed = True
console.print(f" [bold]pyRPC dev[/bold] http://{host}:{port}/rpc")The server_managed flag matters downstream: it gates whether the interactive console’s restart command is allowed to touch the process, and whetherdev terminates anything on exit. When the server was already running, pyRPC is a guest, it attaches, regenerates types, watches files, and leaves your process alone.
The workflow it enables
This single probe is what makes the tool feel non-possessive:
- Two terminals, one server. Start the app with your debugger, then run
pyrpc devin another terminal purely for type regeneration. - Docker-in-the-loop. The server runs in a container;
devon your host detects it over HTTP and stays out of the way. - Safe re-runs. Accidentally running
pyrpc devtwice never results in a port conflict, the second invocation attaches.
Detection is intentionally narrow and fast. It is a hello, not an audit: one GET, one-second budget, and a decision about ownership that the rest of the CLI respects.
Read the full changelogfor the complete list of changes.

pyRPC