May 17, 2026

D-Bus Introspection in Practice: Exploring systemd from the Command Line

Most of the time, we talk to systemd through systemctl. It is convenient, readable and usually exactly what you want.

But systemctl is only one layer above the real API. Under the hood, systemd exposes a D-Bus interface. You can inspect that interface, read properties from it and even call methods directly from the command line.

That is useful when you are debugging an embedded Linux device and want to understand what is really available on the bus. Not what the documentation says. Not what you think is there. What the running system actually exposes.

In this post, we will go one layer below systemctl and look at systemd directly over D-Bus using busctl and dbus-send.

systemctlvsdbus

💡 Tip

systemctl is convenient at the command line, but application code should usually talk to systemd through D-Bus instead of spawning systemctl.

ⓘ Note

This post builds on top of previous article: How D-Bus works in Embedded Linux

The systemd D-Bus entry point


systemd exposes its manager object on the system bus. These are the four coordinates we need:

the systemd d bus entry point

This follows the same D-Bus model as before: service name + object path + interface + method

The service name tells D-Bus where the message should go. The object path tells systemd which object we want to address. The interface tells us which API contract we want to use. The method is the operation we call.

First, check if systemd is visible on the bus:

busctl list | grep systemd

You can ask the bus for all registered names with dbus-send too:

dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.DBus \
  /org/freedesktop/DBus \
  org.freedesktop.DBus.ListNames

💡 Tip

The important part is --system. systemd is not on your user session bus. It is a system service, so it lives on the system bus.

ⓘ Note

For most embedded Linux debugging, the system bus is the one you care about. systemd, NetworkManager, BlueZ, ModemManager and many product-specific daemons usually expose APIs there.

Introspecting the manager object


Introspection means asking a D-Bus object what it exposes. For systemd, start with the manager object: /org/freedesktop/systemd1
With busctl, this is easy to read:

busctl introspect \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1

You should see several interfaces. Some are standard D-Bus interfaces. One is the systemd manager interface:

org.freedesktop.DBus.Introspectable
org.freedesktop.DBus.Properties
org.freedesktop.DBus.Peer
org.freedesktop.systemd1.Manager

The interesting one here is: org.freedesktop.systemd1.Manager
This is where methods such as ListUnits, GetUnit, StartUnit, StopUnit and RestartUnit are exposed. You can also call the introspection method directly with dbus-send:

dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.DBus.Introspectable.Introspect

The output is less friendly than busctl introspect, but it shows an important point: introspection is not magic. It is just another D-Bus method call.

introspectingthemanagerobject

Calling a safe method: ListUnits


A good first method to call is ListUnits. It does not change system state. It only asks systemd to return information about currently loaded units.

With busctl:

busctl call \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  ListUnits

With dbus-send:

dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager.ListUnits

The output is not as pretty as systemctl list-units. That is expected. Here we are not using the human-friendly frontend. We are looking at the raw API.

For quick debugging, you can still use normal shell tools:

dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager.ListUnits | grep ssh

         string "ssh.service"
         object path "/org/freedesktop/systemd1/unit/ssh_2eservice"

💡 Tip

When exploring an unknown D-Bus API, start with read-only methods and properties. Do not begin by calling methods that change system state.

Getting a unit object with GetUnit


In the systemd D-Bus API, a unit is represented as its own object. So if we want to inspect ssh.service, we first ask systemd for the object path of that unit.

 dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager.GetUnit \
  string:"ssh.service"

method return time=1779018852.934374 sender=:1.0 -> destination=:1.28 serial=4282 reply_serial=2
   object path "/org/freedesktop/systemd1/unit/ssh_2eservice"

On some systems, the service may be called sshd.service instead.

The response contains an object path: object path "/org/freedesktop/systemd1/unit/ssh_2eservice". This is a useful detail. GetUnit does not return the state of the unit. It returns the D-Bus object path that represents the unit. Now we can inspect that object directly.

💡 Tip

On some systems:

Reading properties from a unit


busctl introspect \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1/unit/ssh_2eservice \
  org.freedesktop.systemd1.Unit \
  --no-pager \
  | grep -E 'Id|Description|LoadState|ActiveState|SubState|FragmentPath'

ⓘ Note

The final grep -E is only there to keep the output readable. busctl introspect can print a lot of methods, properties and signals, so here we filter the output to the few unit properties we want to discuss.

This interface exposes useful properties, for example:

.ActiveState                     property  s         "active"                                 emits-change
.Description                     property  s         "OpenBSD Secure Shell server"            const
.FragmentPath                    property  s         "/lib/systemd/system/ssh.service"        const
.Id                              property  s         "ssh.service"                            const
.LoadState                       property  s         "loaded"                                 const
.SubState                        property  s         "running"                                emits-change

Now we can read individual properties directly with busctl get-property.

For example, read ActiveState:

busctl get-property \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1/unit/ssh_2eservice \
  org.freedesktop.systemd1.Unit \
  ActiveState

The output may look like this:

s "active"

The first letter is the D-Bus type signature. Here s means string, and "active" is the current value.

You can read SubState the same way:

busctl get-property \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1/unit/ssh_2eservice \
  org.freedesktop.systemd1.Unit \
  SubState

Example output:

s "running"

You can also check whether the unit was loaded correctly:

busctl get-property \
  org.freedesktop.systemd1 \
  /org/freedesktop/systemd1/unit/ssh_2eservice \
  org.freedesktop.systemd1.Unit \
  LoadState

Example output:

s "loaded"

💡 Tip

Properties are often the best starting point. Before calling StartUnit, StopUnit or RestartUnit, check what systemd already thinks about the unit through ActiveState, SubState and LoadState.

Calling methods that change state


⚠ Warning

The next commands change system state. Run them only on a development machine, test device or with a service that is safe to start and stop.

Starting, stopping or restarting system services may require elevated privileges. If you run the command as an unprivileged user, systemd may reject it with org.freedesktop.DBus.Error.InteractiveAuthorizationRequired. On a development machine, run the command with sudo or choose a test unit that your user is allowed to control.

To start a unit through D-Bus, call StartUnit on the systemd manager interface:

dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager.StartUnit \
  string:"ssh.service" \
  string:"replace"

The arguments are simple:

string:"ssh.service"   unit name
string:"replace"       job mode

To stop the unit:

dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager.StopUnit \
  string:"ssh.service" \
  string:"replace"

To restart it:

dbus-send \
  --system \
  --print-reply \
  --dest=org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager.RestartUnit \
  string:"ssh.service" \
  string:"replace"

This is the same idea as:

systemctl start ssh.service
systemctl stop ssh.service
systemctl restart ssh.service

The difference is that now we are calling the D-Bus API directly instead of using the systemctl frontend.

callingmethodsthatchangestate

Watching systemd signals


D-Bus is not only method calls. Services can also emit signals when something changes.

systemd emits signals when units are added, removed or changed. You can watch those messages with busctl:

busctl monitor org.freedesktop.systemd1

Or with dbus-monitor:

dbus-monitor \
  --system \
  "sender='org.freedesktop.systemd1'"

Then trigger a change from another terminal:

systemctl restart ssh.service

This is useful when debugging startup ordering, failing services, watchdog restarts or unexpected unit state changes.

watchingsystemdsignals

A practical workflow


When exploring a D-Bus API, do not guess. Walk through it step by step.

  1. Find the service name with busctl list.
  2. Introspect the manager object with busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1.
  3. Find the interface you need, usually org.freedesktop.systemd1.Manager.
  4. Call safe read-only methods first, such as ListUnits.
  5. Use GetUnit to obtain a unit object path.
  6. Introspect the unit object.
  7. Read properties such as ActiveState, SubState and LoadState.
  8. Call state-changing methods carefully.
  9. Monitor signals while changing state.

This workflow works well beyond systemd. You can use the same approach with BlueZ, NetworkManager, ModemManager and your own embedded services.

workflowsystemd

Common mistakes


A few small mistakes can make D-Bus debugging more confusing than it needs to be.

  • Using the wrong bus. systemd lives on the system bus, so use --system. Do not use --session and expect to reach the system manager.
  • Mixing up service, object, interface and method. These strings look similar, but they are different parts of the call.
  • Guessing escaped unit paths. A unit name such as ssh.service may become /org/freedesktop/systemd1/unit/ssh_2eservice. Use GetUnit and let systemd return the correct object path.
  • Changing state before reading state. Before calling StartUnit, StopUnit or RestartUnit, check ActiveState, SubState and LoadState first.

💡 Tip

When something fails, check the bus, object path and permissions first. Many D-Bus problems are not caused by the method itself, but by calling it on the wrong bus, wrong object or without the required privileges.

The key idea


If you remember only one thing, remember this:

💡 Tip

D-Bus introspection lets you ask a running service what it exposes, then call that API step by step instead of guessing.

For systemd, start at org.freedesktop.systemd1, inspect /org/freedesktop/systemd1, use the manager interface, get a unit object path and read properties before changing anything.

Once this workflow becomes familiar, D-Bus stops looking like a hidden mechanism behind systemctl. It becomes a normal local API that you can inspect, call and monitor from the Linux command line.