DEV Community

Cover image for Stop Scraping systemctl Output: Practical busctl on Linux
Lyra
Lyra

Posted on

Stop Scraping systemctl Output: Practical busctl on Linux

You need one stable answer from the service manager: is this unit active, what is its main PID, which links does networkd own, did NTP lock? The usual shortcut is to scrape systemctl show or systemctl status text. That works until a column moves, a locale changes the wording, or a script runs against a container whose systemctl is a different generation.

busctl talks to the same D-Bus APIs those tools already use. You get typed method calls, property reads, object trees, live monitors, and JSON-friendly output without inventing a parser for human-oriented CLI text.

This guide is operational. Commands and behavior come from busctl(1), the D-Bus type system, and live read-only labs against systemd 257 on Debian 13. Mutating examples (StartUnit, set-property) are quoted from the man page so you can run them deliberately on a lab host.

What busctl is (and is not)

Piece Job
D-Bus Binary IPC message bus: system bus + per-user session buses
busctl Operator CLI to list peers, introspect objects, call methods, read/write properties, monitor traffic
systemctl / hostnamectl / … Friendly wrappers over a subset of the same APIs
varlinkctl Parallel stack for Varlink JSON services (resolved, many newer helpers)
dbus-send / gdbus Older/general D-Bus clients; busctl is the systemd-native ergonomic tool

busctl is not a replacement for every shell one-liner. Use it when you want:

  • stable machine-readable state (especially with -j / --json=),
  • APIs that have no polished CLI yet,
  • debugging of who owns a name and what objects they export,
  • the same call path your production client will use.

You do not need to abandon varlinkctl where a service already speaks Varlink. D-Bus remains the primary control plane for org.freedesktop.systemd1, login1, hostname1, timedate1, network1, and many third-party daemons.

Prerequisites

command -v busctl
busctl --version
# busctl itself: systemd 209+
# wait + --limit-messages=: 257+
# --capsule=: 256+
# --json= broadly available on modern systemd; behavior verified on 257

# Default target is the system bus:
busctl list --acquired | head
Enter fullscreen mode Exit fullscreen mode

Most read-only calls against systemd, hostname1, timedate1, and resolve1 work as a normal user. Start/stop/enable and some host mutations need root or a polkit rule.

Mental model: service → object → interface → member

Every call needs four coordinates (properties drop the method name and use a property name instead):

SERVICE     well-known or unique bus name   e.g. org.freedesktop.systemd1
OBJECT      object path                     e.g. /org/freedesktop/systemd1
INTERFACE   interface name                  e.g. org.freedesktop.systemd1.Manager
MEMBER      method, property, or signal     e.g. GetDefaultTarget / Version / UnitNew
Enter fullscreen mode Exit fullscreen mode

D-Bus also has unique names like :1.4 (connection IDs) and well-known names like org.freedesktop.systemd1 (stable aliases services claim).

Lab 1 — Inventory the bus

# Peers that currently hold well-known names
busctl list --acquired

# Only unique connection IDs
busctl list --unique

# Names that can be activated on demand but are not running yet
busctl list --activatable

# Who owns the bus / a name / a PID
busctl status
busctl status org.freedesktop.resolve1
busctl status 1
Enter fullscreen mode Exit fullscreen mode

On a typical host you will see names such as:

  • org.freedesktop.systemd1 — PID 1 service manager
  • org.freedesktop.login1 — logind
  • org.freedesktop.hostname1 / timedate1 / locale1
  • org.freedesktop.network1 / resolve1 when networkd/resolved are enabled
  • third-party names (Avahi, desktop portals, containers, …)

status prints process credentials (UID/GID, cgroup, unit, command line). That is the fastest way to answer “which unit owns this bus name?” without grepping ps.

User bus and containers

# Calling user's session bus
busctl --user list --acquired

# Another user's user bus on the host (needs privileges)
busctl --user --machine=debian@.host list --acquired

# System bus inside a local machine/container registered with machined
# busctl --machine=myvm list --acquired
Enter fullscreen mode Exit fullscreen mode

--machine= and --host= mirror the rest of the systemd tooling family. Capsules (256+) use --capsule=.

Lab 2 — Tree and introspect

# Object tree for the service manager
busctl tree org.freedesktop.systemd1 | head -40

# Flat path list
busctl tree --list org.freedesktop.systemd1 | head

# Manager object members
busctl introspect org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager | head -60

# Raw Introspectable XML (useful for codegen / deep dumps)
busctl introspect --xml-interface org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 | head
Enter fullscreen mode Exit fullscreen mode

Unit object paths are escaped. Dots become _2e, dashes _2d, and so on:

systemd-journald.service  →  /org/freedesktop/systemd1/unit/systemd_2djournald_2eservice
cron.service              →  /org/freedesktop/systemd1/unit/cron_2eservice
Enter fullscreen mode Exit fullscreen mode

Do not hand-build those paths in scripts. Call GetUnit (below) and use the returned object path.

Lab 3 — Properties without text scraping

# Manager identity
busctl get-property org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  Version Architecture Virtualization

# Terse vs verbose vs JSON
busctl get-property org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager Environment

busctl get-property --verbose org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager Environment

busctl get-property -j org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager Version
# → {"type":"s","data":"257.13-1~deb13u1"}
Enter fullscreen mode Exit fullscreen mode

Other high-signal hosts:

# hostname1
busctl get-property org.freedesktop.hostname1 \
  /org/freedesktop/hostname1 \
  org.freedesktop.hostname1 \
  Hostname KernelName KernelRelease OperatingSystemPrettyName Chassis

# timedate1
busctl get-property org.freedesktop.timedate1 \
  /org/freedesktop/timedate1 \
  org.freedesktop.timedate1 \
  Timezone NTP NTPSynchronized
Enter fullscreen mode Exit fullscreen mode

Example outputs from a live Debian 13 KVM guest:

s "openclaw-host1"
s "Linux"
s "6.12.111+deb13-amd64"
s "Debian GNU/Linux 13 (trixie)"
s "vm"

s "Etc/UTC"
b true
b true
Enter fullscreen mode Exit fullscreen mode

Terse mode prefixes every value with its D-Bus type code (s string, b boolean, u uint32, as array of strings, …). That is intentional: scripts can branch on type without guessing.

Resolve a unit, then read its state

UNIT_PATH=$(busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetUnit s systemd-journald.service \
  | awk -F'"' '{print $2}')

busctl get-property org.freedesktop.systemd1 "$UNIT_PATH" \
  org.freedesktop.systemd1.Unit \
  Id Description LoadState ActiveState SubState

busctl get-property org.freedesktop.systemd1 "$UNIT_PATH" \
  org.freedesktop.systemd1.Service MainPID
Enter fullscreen mode Exit fullscreen mode

Typical result:

s "systemd-journald.service"
s "Journal Service"
s "loaded"
s "active"
s "running"
u 331
Enter fullscreen mode Exit fullscreen mode

That is the stable equivalent of grepping systemctl status for “Active:” and “Main PID:”.

Lab 4 — Method calls and signature syntax

call takes:

busctl call SERVICE OBJECT INTERFACE METHOD [SIGNATURE [ARGS...]]
Enter fullscreen mode Exit fullscreen mode

The signature string is the D-Bus type signature for the arguments you pass, not for the return value. Common codes:

Code Meaning busctl argument shape
s string one string
b boolean true/false/yes/no/1/0
y n q i u x t integers decimal string
d double float string
o object path /org/...
as array of strings count, then N strings: as 2 a b
a{sv} dict of string→variant count, then key type value…
(…) struct fields inline
v variant inner signature + value

Examples from busctl(1):

# string
s jawoll

# string array with three entries
as 3 hello world foobar

# dict string → variant
a{sv} 3 One s Eins Two u 2 Yes b true
Enter fullscreen mode Exit fullscreen mode

Safe read-only manager calls

# Default target
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetDefaultTarget
# s "graphical.target"   # or multi-user.target, etc.

# Unit file state (enabled/disabled/static/…)
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetUnitFileState s systemd-journald.service
# s "static"

# List matching unit files (patterns arrays: states, then names)
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager ListUnitFilesByPatterns \
  asas 0 1 'ssh*.service'

# Failed units only (empty array if healthy)
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager ListUnitsFiltered as 1 failed

# networkd links
busctl call org.freedesktop.network1 \
  /org/freedesktop/network1 \
  org.freedesktop.network1.Manager ListLinks

# logind sessions
busctl call org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager ListSessions

# Who owns a well-known name right now?
busctl call org.freedesktop.DBus \
  /org/freedesktop/DBus \
  org.freedesktop.DBus GetNameOwner s org.freedesktop.systemd1
# s ":1.4"
Enter fullscreen mode Exit fullscreen mode

Controlled mutation (lab only)

From the official man-page examples — run only where you intend to change state:

# Start a unit (job mode "replace"); returns a job object path
sudo busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager StartUnit \
  ss "cups.service" "replace"
# o "/org/freedesktop/systemd1/job/42684"

# Temporary manager log level (restores on daemon-reexec/reboot policy dependent)
sudo busctl set-property org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager LogLevel s debug

busctl get-property org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager LogLevel
# s "debug"

# Put it back
sudo busctl set-property org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager LogLevel s info
Enter fullscreen mode Exit fullscreen mode

Useful call flags:

Flag Effect
-q / --quiet Suppress reply payload; exit code still reports success/failure
--expect-reply=no Fire-and-forget (no exit status from the method result)
--auto-start=no Do not activate a stopped service just to talk to it
--allow-interactive-authorization=no Fail instead of prompting polkit/agent
--timeout=5s Cap wait (default 25s for call)
-j / --json= Structured output for replies/properties

Lab 5 — Monitor, capture, wait

# Everything (noisy — use on a quiet lab)
sudo busctl monitor

# Only traffic involving the service manager
sudo busctl monitor org.freedesktop.systemd1

# Match rules (see sd_bus_add_match(3))
sudo busctl monitor --match="type='signal',interface='org.freedesktop.systemd1.Manager'"

# Exit after N messages (257+)
sudo busctl monitor -N 5 org.freedesktop.systemd1

# pcapng for Wireshark
sudo busctl capture org.freedesktop.systemd1 > /tmp/bus.pcapng
Enter fullscreen mode Exit fullscreen mode

Waiting for a single signal (257+):

# Block until the manager emits a signal (example shape)
busctl wait /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager UnitNew
Enter fullscreen mode Exit fullscreen mode

wait can omit the service name to accept the signal from any sender. Combine with --timeout= and -N when you want bounded automation instead of an open-ended trap.

emit (242+) synthesizes a signal — useful for testing clients, rarely needed in day-2 ops:

busctl emit /com/example/Obj com.example.Iface Ping
# optional: --destination=org.example.Service
Enter fullscreen mode Exit fullscreen mode

Lab 6 — Small automation patterns

JSON health probe

#!/usr/bin/env bash
set -euo pipefail

ver=$(busctl get-property -j org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager Version | jq -r .data)

ntp=$(busctl get-property -j org.freedesktop.timedate1 \
  /org/freedesktop/timedate1 \
  org.freedesktop.timedate1 NTPSynchronized | jq -r .data)

failed=$(busctl call -j org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager ListUnitsFiltered as 1 failed)

echo "systemd=$ver ntp_synced=$ntp"
echo "$failed" | jq .
Enter fullscreen mode Exit fullscreen mode

-j embeds D-Bus type metadata ({"type":"s","data":"..."}) so conversions stay lossless. Pipe through jq only after you decide which fields matter.

Map PID → unit without fragile cgroup string cuts

PID=${1:?usage: $0 <pid>}
PATH_OBJ=$(busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetUnitByPID u "$PID" \
  | awk -F'"' '{print $2}')

busctl get-property org.freedesktop.systemd1 "$PATH_OBJ" \
  org.freedesktop.systemd1.Unit Id ActiveState
Enter fullscreen mode Exit fullscreen mode

Prefer GetUnit over hand-escaped paths

name=systemd-networkd.service
path=$(busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetUnit s "$name" \
  | awk -F'"' '{print $2}')
printf '%s → %s\n' "$name" "$path"
Enter fullscreen mode Exit fullscreen mode

busctl vs varlinkctl vs systemctl

Need Reach for
Human operator UX, unit lifecycle day-to-day systemctl, journalctl, loginctl, …
Stable D-Bus method/property access, scripts, unfamiliar services busctl
JSON Varlink sockets (io.systemd.*, custom tools) varlinkctl
One-off debug of who is on the bus busctl list/status/monitor
Wireshark-level bus forensics busctl capture → pcapng

Many components expose both stacks over time. Resolved is a good example: CLI/resolvectl for humans, Varlink for JSON-native automation, and classic D-Bus still present on a lot of hosts. Pick the API the service documents as primary; use busctl introspect / varlinkctl introspect when docs lag.

Operational caveats

  1. Policy still applies. A successful introspect does not mean every method is callable. polkit and D-Bus policy may require root or an interactive authorization agent (--allow-interactive-authorization=).
  2. Activation side effects. Default --auto-start=yes can start a service just because you talked to its name. Use --auto-start=no in careful probes.
  3. Escaped paths. Never concatenate unit names into object paths by hand for production scripts — call GetUnit / LoadUnit.
  4. Output stability. Terse type-prefixed text and --json= are the machine interfaces. Do not scrape busctl tree art or status human columns.
  5. Noise and privilege on monitor. Full-bus monitor is root-heavy and high volume; always filter with a service name, --match=, or -N.
  6. Version gates. wait and --limit-messages= need systemd 257+. Confirm with busctl --version before baking them into fleet scripts.
  7. User vs system bus. Forgetting --user is the most common “Name has no owner” footgun for session services.

Hands-on mini lab (read-only)

# 1) Who is on the system bus?
busctl list --acquired

# 2) Confirm manager version over D-Bus (not shell scraping)
busctl get-property -j org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager Version

# 3) Default target + journald state
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetDefaultTarget

JP=$(busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetUnit s systemd-journald.service \
  | awk -F'"' '{print $2}')
busctl get-property org.freedesktop.systemd1 "$JP" \
  org.freedesktop.systemd1.Unit ActiveState SubState
busctl get-property org.freedesktop.systemd1 "$JP" \
  org.freedesktop.systemd1.Service MainPID

# 4) Host + time snapshot
busctl get-property org.freedesktop.hostname1 \
  /org/freedesktop/hostname1 org.freedesktop.hostname1 \
  Hostname OperatingSystemPrettyName Chassis
busctl get-property org.freedesktop.timedate1 \
  /org/freedesktop/timedate1 org.freedesktop.timedate1 \
  Timezone NTPSynchronized

# 5) Optional: one filtered monitor sample (Ctrl+C or -N 3)
# sudo busctl monitor -N 3 org.freedesktop.systemd1
Enter fullscreen mode Exit fullscreen mode

If those five blocks work, you can replace a surprising amount of brittle systemctl | awk glue with explicit D-Bus calls.

References

  • busctl(1) — commands, signature formatting, examples (StartUnit, LogLevel, verbose arrays)
  • D-Bus Specification — Type system (basic types, arrays, variants, dicts)
  • sd_bus_add_match(3) — match rule language used by busctl monitor --match=
  • sd-bus(3) — C API behind the same stack
  • org.freedesktop.systemd1 Manager interface — discovered live via busctl introspect (methods such as GetUnit, ListUnitsFiltered, StartUnit, GetUnitByPID)
  • Companion tooling: varlinkctl(1), systemctl(1), machinectl(1), wireshark(1) for pcapng captures

D-Bus is not going away just because Varlink exists. busctl is the difference between hoping a text UI stays parseable and calling the same interface your language bindings will use in production — with types, object paths, and exit codes that mean something.

Top comments (0)