In this journal, we will take a look on how to minimize idle services running in the background using systemd-socket-proxyd(8) for services that we cannot control their code and using systemd.socket(5) for services that we can changes their code.
Background
Lets take a look running processes on my host machine (click on the image to enlarge):

At the top, we have a process with 261.3 gigabytes virtual memory address. This service rarely used, only once a day or less.
Below that, we have HTTP services that I manage for local development using
systemd user services.
For example, this website has their own local domain kilabit.local that
run on specific port.
My goal is to reduce the running processes and activate it only when I opened the port or domain in the browser or when some other services connect to the port directly.
As for measurement, we count number of tasks using ps, memory using
free, and boot time using systemd-analyze after booting and login.
Here is the initial value,
$ ps -e | wc -l
198
$ free -m
total used free shared buff/cache available
Mem: 15620 3562 10370 142 2132 12058
Swap: 4095 0 4095
$ systemd-analyze
Startup finished in 6.868s (firmware) + 2.737s (loader) + 934ms (kernel) + \
1.693s (initrd) + 6.198s (userspace) = 18.432s
graphical.target reached after 6.193s in userspace.
$
Lets start with external services first.
Using systemd-socket-proxyd
For external services where I cannot changes their code, I use systemd-socket-proxyd.
systemd-socket-proxyd is a generic socket-activated network socket forwarder proxy daemon for IPv4, IPv6 and UNIX stream sockets. It may be used to bi-directionally forward traffic from a local listening socket to a local or remote destination socket.
https://man.archlinux.org/man/systemd-socket-proxyd.8
For example, let say I have service X that listen on port 8080.
We will let the systemd.socket listen to that port and run the actual
service on port 18080.
Any other services still connect to port 8080 to communicate to service X
through systemd-socket-proxyd.
First, we create the /etc/systemd/system/proxy-X.socket that will listen
on port 8080,
[Socket] ListenStream=127.0.0.1:8080 [Install] WantedBy=sockets.target
Next, we create the /etc/systemd/system/proxy-X.service that will run
systemd-socket-proxyd and channel the traffic to port 18080,
[Unit] Requires=X.service Requires=proxy-X.socket After=X.service After=proxy-X.socket [Service] Type=notify ExecStartPre=/bin/sleep 3 ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=5m \ 127.0.0.1:18080 ExecStopPost=systemctl stop X.service
Note that, in this case, we add the ExecStartPre= to wait for X.service
run successfully, and the ExecStopPost= to stop the service.
Both of these may or may not needed, depends on the service.
In the X.service, we override the service unit file to run on port 18080
instead of default port,
# /etc/systemd/system/X.service.d/override.conf [Unit] StopWhenUnneeded=true [Service] ExecStart= ExecStart=/usr/bin/X --Port 18080
With —exit-idle-time=5m and StopWhenUnneeded=true, we tell the systemd
to stop the service when no traffic received after 5 minutes.
Now stop the actual service and start the proxy-X.socket service,
$ sudo systemctl disable --now X.service $ sudo systemctl enable --now proxy-X.socket $
We test it by opening the service port :8080 in the browser.
Here is sample log in the journal,
Feb 01 18:20:11 systemd[1]: Started X Daemon. Feb 01 18:20:11 systemd[1]: Starting proxy-X.service... <TRUNCATED> Feb 01 18:20:14 X[8144]: Now listening on: http://127.0.0.1:8080 Feb 01 18:20:14 systemd[1]: Started proxy-X.service.
After 5 minutes we will the service stopped automatically,
Feb 01 18:25:34 systemd[1]: proxy-X.service: Deactivated successfully. Feb 01 18:25:34 systemd[1]: Stopping X Daemon... Feb 01 18:25:34 X[8144]: Application is shutting down... Feb 01 18:25:34 X[8144]: 02-01 18:25:34 Info X stopped Feb 01 18:25:34 systemd[1]: X.service: Deactivated successfully. Feb 01 18:25:34 systemd[1]: Stopped X Daemon. Feb 01 18:25:34 systemd[1]: X.service: Consumed 4.010s CPU time over 5min 23.145s wall clock time, 163.1M memory peak.
Great. We can repeat the same configurations to other services.
Using systemd.socket
Now, the hard part is to rewrite all of the programs so it can be activated using systemd.socket(5).
All of the program that we want to convert are built with Go. In the previous journal, I have describe how to modify the Go program so it can be activate using systemd.socket(5). If you have a program that need to be converted too, but not written using Go, you can read the following blog first to understand the basic,
We will modify each of the main function in the program. There are at least 11 of them. Fortunately, most of them are using the same library/package.
-
local.awwan.service ⇒ ciigo based. This serve the local version of https://awwan.org .
-
local.golang-id.service ⇒ ciigo based. This serve the local version of https://golang-id.org .
-
local.golang-id-tour.service ⇒ net/http based. This serve the local version of https://tour.golang-id.org .
-
local.home.bahasa-go.service ⇒ ciigo based.
-
local.home.gitdoc.service ⇒ httpdfs ⇒ lib/http based. This serve the Git documentation directory
-
local.home.go.service ⇒ net/http based. This run golang.org/x/website for offline documentation (the same content as https://go.dev ). You can view the patch here (subject to changes in the future).
-
local.home.godoc.service ⇒ net/http based. This run golang.org/x/pkgsite with local modules. You can see the patch here (subject to changes in the future).
-
local.home.gorankusu.service ⇒ lib/http based.
-
local.home.ops.service ⇒ lib/http based
-
local.kilabit.service ⇒ ciigo based.
-
local.kilabit.umum.service ⇒ httpdfs ⇒ lib/http based.
Most of the modification are adding the following identical code,
...
listeners, err := systemd.Listeners(true)
if err != nil {
log.Fatal(err)
}
if len(listeners) > 1 {
log.Fatal(`too many listeners received for activation`)
}
var listener net.Listener
if len(listeners) == 1 {
listener = listeners[0]
gotAddr := listener.Addr().String()
if gotAddr != serveOpts.Address {
log.Fatalf(`invalid Listener address, got %s, want %s`,
gotAddr, serveOpts.Address)
}
}
// Pass the listener to HTTP Server.
...
Results
Let’s reboot the machine and see the results.
$ diff -u before after
--- before 2026-02-02 23:52:11.677244743 +0700
+++ after 2026-02-02 23:52:25.315331792 +0700
@@ -1,12 +1,12 @@
$ ps -e | wc -l
-198
+182
$ free -m
total used free shared buff/cache available
-Mem: 15620 3562 10370 142 2132 12058
+Mem: 15620 2898 12491 102 601 12722
Swap: 4095 0 4095
$ systemd-analyze
-Startup finished in 6.868s (firmware) + 2.737s (loader) + 934ms (kernel) + \
- 1.693s (initrd) + 6.198s (userspace) = 18.432s
-graphical.target reached after 6.193s in userspace.
+Startup finished in 6.804s (firmware) + 1.438s (loader) + 985ms (kernel) + \
+ 1.508s (initrd) + 5.985s (userspace) = 16.722s
+graphical.target reached after 5.983s in userspace.
The number of processes started on boot reduced by 16,
the total memory used reduced by 664 MB (-18%), and
the boot time reduced by 1.71 second (-9%).
Well, it is not so bad. The big win is in the reduced memory usage and
learning how to use systemd.socket and systemd-socket-proxyd.