Back to work

Case Study · Server Ops Tooling

palctl

A REST-native control plane for a Palworld dedicated server that restarts on the symptom, not the clock— it reads the game server's actual memory off the OS and forecasts the leak curve, so it can restart early while the server is empty instead of at the threshold with players mid-session. One engineer, five surfaces over one core.

StatusReleased · v1.0.0
RoleSolo — design + engineering
RuntimePython 3.11+ · PySide6
Surfacesdaemon · GUI · CLI · web · Discord
PlatformsWindows · headless Linux
LicenseAGPL-3.0-or-later (+ commercial)

There are good Palworld managers. This one is different in three specific ways.

Every other Windows GUI drives RCON, which Pocketpair has deprecated. palctl uses the recommended REST API— which also unlocks what RCON simply can't report: server FPS, frame time, uptime, base-camp count, and per-player level, ping, location, and building count.

Palworld's dedicated server leaks memory. The universal advice is “restart it on a timer,” which either kicks people for no reason or leaves the server a slideshow for hours before the timer fires.

palctl reads the server process's actual resident memoryfrom the OS and restarts when it's really bloating — with a countdown, a world save, and a hold-off while players are online. You can't do that from a web panel or a cloud bridge. You have to be on the box — which is also why the Discord bot is self-hosted: your token, your machine, no third party holding your admin password.

Two processes, five surfaces, one shared core.

The split is deliberate. The daemon is the manager — wrapped in a Windows service or a systemd unit, it survives reboots and sign-outs. The GUI is a viewer you open when you want it: closing the window manages nothing worse. Everything a user touches — desktop, CLI, web, Discord — drives the same token-gated daemon API, so the four surfaces can never disagree about what the server is doing.

DaemonThe always-on brain

A headless process running five supervised async loops under one asyncio.gather — poll/diff, memory watchdog, scheduler, leak forecaster, update-check — plus the Discord bot and the control API. One escaped exception is logged and contained; the other loops keep running instead of the whole daemon going down.

GUIPySide6 desktop

Dashboard (FPS, frame time, a memory sparkline with the watchdog limit drawn in, uptime, in-game day, base camps), Players with kick/ban, a Console, and a typed, grouped Settings editor over the one-line OptionSettings blob. Console actions run on a worker thread, so a slow service call can’t freeze the window.

CLI16 subcommands over the API

status, players, events, start/stop/restart, save, backup/backups/restore, update, announce, kick/ban/unban, and ui — all talking to the daemon’s token-gated localhost API. Because it rides the same API as everything else, it works over ssh, in cron, and on the headless box the desktop GUI can’t serve. palctl kick zoe resolves the name to a user ID for you.

WebA dashboard from your phone

One self-contained page the daemon serves on 127.0.0.1 only — live status, players, the memory sparkline, events, and the full control set with confirm-gated restore. The per-user token rides the URL fragment, so it never leaves the browser. Remote access goes over an ssh tunnel or Tailscale, never an exposed port.

DiscordA bot that’s yours

13 slash commands plus join/leave, level-up, watchdog, up/down, and update-available notifications, an optional auto-refreshing status message, and a welcome line. Your token, your machine — no third-party bridge holding your admin password. First-connect retry, and sends are queued so a Discord rate limit can’t stall polling or the watchdog.

CorePlatform-neutral, tested anywhere

The ini parser, backups, session/metrics tracking (SQLite), scheduler, path detection, SteamCMD orchestration, the operation lock, the watchdog hold-off, and the leak forecaster are all OS-agnostic and unit-tested on any OS. Only service control, the downloads, and the GUI need Windows — which is why the same daemon runs headless on Linux.

The recurring theme is restraint: an auto-restarter that misfires is worse than none.

Restart on the symptom, not the clockThe universal advice is “restart on a timer,” which either kicks people for nothing or leaves the server a slideshow for hours. palctl reads PalServer-Win64-Shipping.exe’s actual resident memory via psutil and restarts when it’s really bloating — guarded by consecutive-sample confirmation, a player hold-off, a hard limit that overrides the hold-off, and a cooldown so it can’t loop.
Forecast the leak, restart while emptyA least-squares fit over recent memory samples answers “how long until the limit at this pace?” — judged from the fitted line, not a lone spike. Opt-in, it restarts early at a moment the server happens to be empty instead of at the threshold two hours later with six people mid-boss. It’s deliberately conservative: no forecast under 12 points, under a 15-minute span, or beyond 24h out.
One operation lockScheduled restarts, watchdog restarts, SteamCMD updates, restores, crash recovery, and your own Start/Stop all funnel through a single named lock. Human-requested work queues; opportunistic automation skips-if-busy and re-evaluates next poll — so a background restart can never fire into the middle of an update or a restore.
A backup that survives the diskBackups copy through a .partial sibling + atomic rename, so an interrupted copy never looks finished; a torn backup is detected by fingerprint, retried, then flagged-but-kept. They can mirror to a second disk or a network share — because a backup on the server’s own disk doesn’t survive that disk. And an update won’t run if its pre-update world backup failed: no rollback path, no update.
Localhost-only, token-gated controlThe daemon binds 127.0.0.1 only, and every request carries a per-user token compared with a constant-time check. The token lives in a 0600 file created with O_EXCL. Defence-in-depth on top of the real boundary — never port-forwarded — so “anyone logged in” still can’t drive stop, restore, kick, or ban.
REST, not deprecated RCONEvery other Windows GUI drives RCON; palctl uses Pocketpair’s recommended REST API, which unlocks what RCON can’t: server FPS, frame time, uptime, base-camp count, and per-player level, ping, location, and building count. It also guards PalWorldSettings.ini across SteamCMD validate — the exact step that blanks it.
5Control surfaces over one shared core
29Commands — 16 CLI subcommands + 13 Discord slash commands
180+Tests green on a Windows + Linux CI matrix (Python 3.11 & 3.12)
1.0.0Shipped after a full release-readiness audit of every subsystem

The stack, by layer.

RuntimePython 3.11+, an asyncio daemon running five supervised concurrent loops plus the bot and API; one crashed loop can’t take the process down.asyncio
DesktopPySide6 (Qt) — dashboard, players, console, a typed settings editor over the OptionSettings blob, and a first-run wizard; console work on worker threads.PySide6
REST / HTTPhttpx client for the Palworld REST API (FPS, frame time, per-player data); aiohttp serves the token-gated 127.0.0.1 control API and the web dashboard.httpx + aiohttp
DataSQLite for session/playtime and metrics history, so the graphs survive a daemon restart; a hand-rolled ini parser that preserves unknown keys from future patches.SQLite
DiscordA self-hosted discord.py bot — 13 slash commands and event notifications, queue-backed sends, and first-connect retry.discord.py
PackagingPyInstaller builds the binaries; Inno Setup produces the installer and a portable zip; secrets go to Windows Credential Manager via DPAPI, never a config file.PyInstaller + Inno Setup
CI / QualityA GitHub Actions matrix (Windows + Linux × Python 3.11/3.12), ruff, an import-smoke job under offscreen Qt, and the test suite on every push.GitHub Actions · ruff

Scoped to what the API actually supports, and honest about the rest.

No chat relayPalworld exposes no chat-read endpoint and dedicated servers ship no log by default. Mirroring chat into Discord is RE-UE4SS territory, not the supported API — so palctl doesn’t pretend to.
No entity / base managerThe gamedata endpoint is in the docs, but there’s currently no way to enable it on a dedicated server — no INI setting, no launch argument. So there’s nothing honest to build on yet.
No plugin frameworkThe server is a closed UE5 binary. There is no Torch equivalent and can’t be one without injection. palctl stays inside what the real API supports.

The installer isn't code-signed yet, so Windows SmartScreen shows a one-time prompt — every release ships a SHA256SUMS.txt so the download can be verified against what CI built. A wingetlisting and free code-signing via SignPath's open-source program are on the roadmap.

On the box.
Where server ops actually live.