unit types beyond .service
Anyone who only knows systemd through systemctl start and systemctl status is seeing just a fraction of the init system. Target units structure the boot process as a directed graph, socket units start services only on the first connection, and both concepts work together with timer units to save resources and model dependencies cleanly.
Table of Contents
- 1. Why targets and sockets are more than calendar jobs
- 2. Unit file anatomy: sections and unit types at a glance
- 3. Understanding targets: synchronization points instead of runlevels
- 4. Creating custom targets and switching with isolate
- 5. Understanding sockets: the principle of socket activation
- 6. Writing a custom socket unit for a service
- 7. Accept=yes versus Accept=no and performance implications
- 8. Interaction: targets, sockets and timers in the boot graph
- 9. Debugging with systemd-analyze and comparison table
- 10. Summary
- 11. FAQ
1. Why targets and sockets are more than calendar jobs
Most beginners first encounter systemd through service units: systemctl start nginx, systemctl enable php8.4-fpm. But the unit types of systemd go far beyond simple services, and especially target units and socket units solve structural problems that classic init systems with sequential runlevel scripts never solved cleanly. A target is not a script but a pure synchronization point in the dependency graph, a socket is not an alternative to a service but an upstream activation mechanism.
Anyone who only knows timer units and uses them to replace cron jobs misses the bigger picture: systemd models the entire system as a graph of units with explicit dependencies, and targets and sockets are the nodes that make this graph robust and parallelizable in the first place. This article deliberately focuses on targets and sockets as standalone unit types and only touches timers where they are embedded in the same dependency graph.
2. Unit file anatomy: sections and unit types at a glance
Every systemd unit file follows the same basic scheme of named sections in square brackets. The [Unit] section describes metadata and dependencies such as Description=, After=, Requires= and Wants=, independent of the concrete unit type. The type specific section, meaning [Service], [Socket], [Timer] or [Mount], contains the actual configuration. The [Install] section finally defines how and where the unit attaches to the dependency tree when activated with systemctl enable, typically through WantedBy=.
Besides the well known .service units there are, among others, .target for synchronization points, .socket for communication endpoints, .timer for time based activation, .mount and .automount for filesystems, as well as .path for activation based on filesystem changes. Each of these types can define its own dependencies, and systemd resolves these dependencies at boot as a parallel graph instead of proceeding strictly sequentially like classic init scripts.
# /etc/systemd/system/example.service — anatomy of a unit file
[Unit]
Description=Example application service
# Ordering and dependency directives, type independent
After=network-online.target
Wants=network-online.target
Requires=example.socket
[Service]
# Type specific section — differs for .service, .socket, .timer, ...
Type=notify
ExecStart=/usr/bin/example-daemon --config /etc/example/config.yaml
Restart=on-failure
RestartSec=5
[Install]
# Determines where this unit attaches when enabled
WantedBy=multi-user.target
3. Understanding targets: synchronization points instead of runlevels
A target is a unit without any executable logic of its own. It has no [Service] or [Socket] section, and instead serves purely as a bundling point for other units. multi-user.target, for example, groups together all units needed for a multi user system state without a graphical interface. graphical.target builds on top of that and adds the display manager. This structure replaces the old SysV runlevels 0 through 6 with named, semantically understandable goals that are additionally freely extensible.
The decisive difference from runlevels lies in parallelism: a runlevel switch worked through scripts strictly one after another in numeric order. A target, in contrast, only defines which units must be reached before the target itself counts as active, and systemd starts all independent units within that goal in parallel. This shortens boot times considerably, because for example network setup and disk mounts, which have no dependency on each other, run at the same time instead of waiting on one another.
4. Creating custom targets and switching with isolate
Custom targets make sense when a group of related services should be addressable as a named unit, for example all components of one application. A custom target unit file usually only needs the [Unit] section with a description and optionally Requires= for mandatory dependencies. With systemctl isolate my-stack.target, systemd stops all units that are not part of this target's dependency tree and starts exactly the units defined there, quite similar to a classic runlevel switch, but with explicit, traceable dependencies instead of numbered scripts.
A practical example: a custom magento-stack.target could bundle php8.4-fpm.service, nginx.service, redis-server.service and mysql.service. A single command systemctl status magento-stack.target then immediately shows whether the entire stack counts as reached, instead of having to run four separate status queries. The PartOf= directive in the individual service units additionally ensures that restarting the target also restarts the associated services.
# /etc/systemd/system/magento-stack.target — custom grouping target
[Unit]
Description=Magento application stack (web, cache, database)
Requires=php8.4-fpm.service nginx.service redis-server.service mysql.service
After=php8.4-fpm.service nginx.service redis-server.service mysql.service
[Install]
WantedBy=multi-user.target
5. Understanding sockets: the principle of socket activation
A socket unit defines a communication endpoint, such as a TCP port, a UNIX socket path or a FIFO, that systemd itself opens and monitors, even before the associated service is running at all. Once a connection arrives at this endpoint, systemd starts the matching service just in time and hands over the already open connection to it. This principle is called socket activation and is conceptually derived from inetd, but was implemented in systemd in a considerably more robust and performant way.
The benefit lies in two areas: first, services only start when actually needed, which saves resources, especially for rarely used services. Second, socket activation decouples boot order from service availability, because the socket already exists as soon as systemd creates it, regardless of whether the actual service has already started. Connecting clients therefore no longer need to wait until the service is fully up, they are merely briefly buffered while systemd starts the process in the background.
6. Writing a custom socket unit for a service
A minimal socket unit consists of a [Socket] section with an endpoint directive, such as ListenStream= for TCP or ListenStream=/run/my-service.sock for a UNIX socket. The associated service unit carries the same base name so systemd automatically recognizes the pairing, for example my-service.socket and my-service.service. It is important that the service unit itself no longer defines a ListenStream, that is fully handled by the socket unit.
In practice this pattern is used, for example, for an internal health check service that should stay idle most of the time. Instead of a permanently running process consuming memory, only the socket managed by systemd itself listens, and the actual process is started only on actual access and can shut down again after a configurable idle period without releasing the port.
# /etc/systemd/system/healthcheck.socket
[Unit]
Description=Socket for the on-demand health check service
[Socket]
ListenStream=127.0.0.1:9100
Accept=no
[Install]
WantedBy=sockets.target
# /etc/systemd/system/healthcheck.service — no ListenStream here
[Unit]
Description=On-demand health check responder
Requires=healthcheck.socket
[Service]
# systemd passes the already-open socket via file descriptor 3
ExecStart=/usr/local/bin/healthcheck-responder
StandardInput=socket
7. Accept=yes versus Accept=no and performance implications
The directive Accept= in the [Socket] section decides a fundamental behavior with noticeable performance consequences. With Accept=no, the default and recommended setting for most use cases, systemd starts exactly one process of the associated service unit, which serves the socket itself via accept() and handles any number of connections internally, much like modern servers such as nginx or PHP-FPM already do on their own. This variant is efficient because no additional process fork per connection is needed.
With Accept=yes, on the other hand, systemd starts a separate instance of the service unit for every single incoming connection, comparable to the classic inetd model. This suits simple, short lived services, such as a basic diagnostic script, but causes considerable overhead through repeated forking and instantiation at high connection rates. For production web applications and database services, Accept=no is practically always the right choice, Accept=yes remains reserved for niche cases with low, irregular load.
8. Interaction: targets, sockets and timers in the boot graph
Targets, sockets and timers are not isolated concepts, they are nodes in the same dependency graph. sockets.target bundles all socket units and is typically reached before basic.target, which ensures that all communication endpoints exist before regular services start. timers.target analogously bundles all timer units. A custom target can specifically reference both socket and timer units via Wants= to model a complete functional area, for example all background tasks of an application including its periodic maintenance jobs.
A timer itself also has a [Unit] section and can depend on a specific target via After= and Requires=, for example to ensure a maintenance timer only becomes active after magento-stack.target, instead of already during the early boot phase. This combination of targets as synchronization points, sockets as on demand entry points, and timers as time triggered activators produces a declarative, robust dependency graph that is clearly superior to classic init scripts in traceability and parallelizability.
# Visualize how a custom target ties services, sockets and timers together
systemctl list-dependencies magento-stack.target
# Show which units belong to sockets.target
systemctl list-dependencies sockets.target
# Confirm a timer is ordered after the application stack target
systemctl show maintenance.timer -p After -p Requires
9. Debugging with systemd-analyze and comparison table
The tool systemd-analyze blame shows which units took the most time during boot, while systemd-analyze critical-chain visualizes the longest dependency chain until a target is reached, which is especially useful for identifying unnecessary serial dependencies between targets. For sockets, systemctl list-sockets provides an overview of all active endpoints along with their associated service unit, and ss -lp adds the operating system view of what is actually listening.
| Unit Type | Purpose | Trigger | Typical Section |
|---|---|---|---|
| .service | Start and monitor a process | manual, dependency, socket, timer | [Service] |
| .target | Bundle a synchronization point | reaching its dependencies | only [Unit] and [Install] |
| .socket | Open an endpoint ahead of time | incoming connection | [Socket] |
| .timer | Time based activation | calendar time or boot offset | [Timer] |
| .path | Filesystem based activation | file appears or changes | [Path] |
This overview shows that targets, sockets and timers each solve different problems and complement each other rather than being alternatives. A well structured system uses all three types deliberately: targets for grouping, sockets for on demand activation, timers for time triggered execution.
Mironsoft
Linux server administration and systemd architecture for PHP hosting
A clean systemd dependency graph instead of grown init scripts?
We model your application stacks as custom targets, set up socket activation for on demand services, and ensure traceable, parallelized boot times on your production servers.
Unit Design
Custom targets for application stacks with clear dependencies
Socket Activation
On demand services instead of permanently running processes
Boot Optimization
Analysis with systemd-analyze and removal of unnecessary serial dependencies
10. Summary
Targets are pure synchronization points without their own executable logic and replace the rigid SysV runlevels with named, parallelizable goals in the dependency graph. Sockets open communication endpoints ahead of time and start the associated service only when actually needed, which saves resources and decouples boot order from service availability. Accept=no is the right choice for most production services, Accept=yes remains reserved for rare, short lived services.
Only in combination do these unit types unfold their full strength: custom targets bundle related services, sockets, and timers into one named, jointly controllable unit. Anyone who views systemd merely as a replacement for /etc/init.d scripts or as a cron alternative misses exactly this structural added value of targets and sockets as standalone, powerful building blocks of the init system.
systemd Targets, Sockets and Timers: The Key Facts at a Glance
Targets
Pure synchronization points without their own process. Replace runlevels with named, parallelizable goals.
Sockets
Open endpoints ahead of time, start services only on demand. Accept=no is the recommended default.
Custom Targets
Bundle related services via Requires= and After=, controllable through systemctl isolate.
Debugging
systemd-analyze blame and critical-chain reveal boot bottlenecks, list-sockets shows active endpoints.