Running urxvt as a daemon/client
An unexpected finding
One evening, while looking for a flag I had forgotten in the urxvt man page, I noticed that it is possible to run urxvt as a daemon/client. The man page mentions it’s more memory efficient and has a faster start time:
It also comes with a client/daemon pair that lets you open any number of terminal windows from within a single process, which makes startup time very fast and drastically reduces memory usage. See urxvtd(1) (daemon) and urxvtc(1) (client).
Both urxvtd and urxvtc are provided by the rxvt-unicode package in Arch Linux:
pacman -Ql rxvt-unicode
rxvt-unicode /usr/
rxvt-unicode /usr/bin/
rxvt-unicode /usr/bin/urclock
rxvt-unicode /usr/bin/urxvt
rxvt-unicode /usr/bin/urxvt-tabbed
rxvt-unicode /usr/bin/urxvtc
rxvt-unicode /usr/bin/urxvtd
[...]
The daemon: urxvtd
urxvtd
runs as a background daemon process and, new terminals can be created by using urxvtc
instead of the usual urxvt
.
There are several ways to do this, the simplest is to run urxvtd
when you start your X11
. Add this to your .xinitrc
or your display manager of choice:
urxvtd --quiet --opendisplay --fork
The following option flags are used:
--quiet
Normally, urxvtd outputs the message “rxvt-unicode daemon listening on path
” after binding to its control socket. This option will suppress this message (errors and warnings will still be logged).
--opendisplay
This forces urxvtd to open a connection to the current $DISPLAY and keep it open.
--fork
This makes urxvtd
fork after it has bound itself to its control socket.
The control socket will be in your home
directory:
ls ~/.urxvt
urxvtd-arch
stat ~/.urxvt/urxvtd-arch
File: urxvtd-arch
Size: 0 Blocks: 0 IO Block: 4096 socket
Device: 259,7 Inode: 6291510 Links: 1
Access: (0700/srwx------) Uid: ( 1000/ twfox) Gid: ( 1000/ twfox)
Access: 2025-08-06 21:19:21.685587252 +0200
Modify: 2025-08-06 21:19:19.567587485 +0200
Change: 2025-08-06 21:19:19.567587485 +0200
Birth: 2025-08-06 21:19:19.567587485 +0200
If you want to use Systemd
instead, you can do so by creating these units. You have these in the urxvt
: FAQ [2]
The urxvtd.service
unit:
urxvtd.service
[Unit]
Description=urxvt terminal daemon
[Service]
ExecStart=/usr/bin/urxvtd -o
[Install]
RequiredBy=urxvtd.socket
And the urxvtd.socket
unit:
urxvtd.socket
[Unit]
Description=urxvt terminal daemon socket
[Socket]
ListenStream=%h/.urxvt/urxvtd-%H
[Install]
WantedBy=sockets.target
You can create these inside /etc/systemd/user/
(since you will be running this for your user session), reload the config, and start/enable as any other Sytemd
units:
systemctl --user daemon-reload
systemctl --user enable urxvtd.socket
systemctl --user start urxvtd.socket
Did we only start the urxvtd.socket? It is configured so urxvtd will start and listen to the socket as soon as it’s needed.
After starting urxvtd.socket
it should look similar to this:
systemctl --user status urxvtd.socket
● urxvtd.service - urxvt terminal daemon
Loaded: loaded (/etc/systemd/user/urxvtd.service; disabled; preset: enabled)
Active: active (running) since Fri 2025-08-08 17:05:15 CEST; 841ms ago
Invocation: 791a5c6158294551bd82635af8847060
TriggeredBy: ● urxvtd.socket
Main PID: 1987452 (urxvtd)
Tasks: 1 (limit: 76875)
Memory: 840K (peak: 1.5M)
CPU: 5ms
CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/urxvtd.service
└─1987452 /usr/bin/urxvtd -o
Aug 08 16:05:15 arch systemd[796]: Started urxvt terminal daemon.
Aug 08 16:05:15 arch urxvtd[1987452]: rxvt-unicode daemon listening on fd.
The client: urxvtc
Now, run the client urxvtc
and check the service:
systemctl --user status urxvtd.service
● urxvtd.service - urxvt terminal daemon
Loaded: loaded (/etc/systemd/user/urxvtd.service; disabled; preset: enabled)
Active: active (running) since Fri 2025-08-08 17:11:30 CEST; 7s ago
Invocation: 5278a3dad36a4c658b2c6e583733f157
TriggeredBy: ● urxvtd.socket
Main PID: 2002293 (urxvtd)
Tasks: 2 (limit: 76875)
Memory: 16.1M (peak: 18.4M)
CPU: 100ms
CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/urxvtd.service
├─2002293 /usr/bin/urxvtd -o
└─2002294 zsh
Aug 08 16:11:30 arch systemd[796]: Started urxvt terminal daemon.
Aug 08 16:11:30 arch urxvtd[2002293]: rxvt-unicode daemon listening on fd.
Notice urxvtd
running with PID 2002293
.
Check if urxvtd
is listening to the socket correctly with lsof
:
lsof -p $(pgrep urxvtd) | egrep 'LISTEN'
urxvtd 2129281 twfox 3u unix 0x000000001fa80fe5 0t0 5039747 /home/twfox/.urxvt/urxvtd-arch type=STREAM (LISTEN)
If something isn’t working you can check the journal entries for the socket and the service.
Normal start/stop cycles should look like this:
journalctl -u urxvtd.socket
Aug 08 16:35:11 arch systemd[1]: Listening on urxvt terminal daemon socket.
Aug 08 16:37:55 arch systemd[1]: urxvtd.socket: Deactivated successfully.
Aug 08 16:37:55 arch systemd[1]: Closed urxvt terminal daemon socket.
journalctl -u urxvtd.service
Aug 08 16:35:11 arch systemd[1]: Started urxvt terminal daemon.
Aug 08 16:35:11 arch urxvtd[1949750]: rxvt-unicode daemon listening on fd.
Aug 08 16:37:49 arch systemd[1]: Stopping urxvt terminal daemon...
Aug 08 16:37:49 arch systemd[1]: urxvtd.service: Deactivated successfully.
Aug 08 16:37:49 arch systemd[1]: Stopped urxvt terminal daemon.
Comparison
Process hierarchy
If you open a few more instances of urxvtc
, you will see a clear difference with the PID tree.
Here we have three terminals running pstree
, ranger
and iftop
with urxvt
.
pstree
├─zsh───urxvt───zsh───pstree
├─zsh───urxvt───zsh───ranger
└─zsh───urxvt───zsh───sudo───sudo───iftop───3*[{iftop}]
And here we have the three same terminals with urxvd
and urxvtc
:
├─urxvtd─┬─zsh───pstree
│ ├─zsh───ranger
│ └─zsh───sudo───sudo───iftop───3*[{iftop}]
The process hierarchy has changed, and now all the urxvt
instances share the same urxvtd
parent process.
Performance
Lets compare the memory footprint. In theory, urxvtd
should help us with the memory footprint as it can more efficiently share memory between the different terminal children processes.
I start the same three urxvt
instances running pstree
, ranger
and iftop
.
ps -o pid,ppid,rss,vsz,pmem,comm -C urxvt
PID PPID RSS VSZ COMMAND
2054884 2054859 22076 50464 urxvt
2054952 2054941 22040 50456 urxvt
2054996 2054985 22216 50460 urxvt
And I do the same with urxvtd
and urxvtc
.
ps -p $(pgrep urxvtd) --ppid $(pgrep urxvtd) -o pid,ppid,rss,vsz,comm
PID PPID RSS VSZ COMMAND
2059496 796 23300 86732 urxvtd
2059497 2059496 8484 13148 zsh
2059566 2059496 8516 13144 zsh
2059652 2059496 8504 13144 zsh
urxvt
(3 terminals):
- Total RSS:
22,076 + 22,040 + 22,216 = 66,332 KB
urxvtd/urxvtc
(3 terminals):
- urxvtd:
23,300 KB
- 3 urxvtc:
8,484 + 8,516 + 8,504 = 25,504 KB
- Total RSS:
48,804 KB
That’s a 26% memory reduction (17.5 MB saved). I don’t know if its “drastic”, but there you go.
For the startup time benchmark, I will be using hyperfine [3].
hyperfine 'urxvt -e /bin/true' 'urxvtc -e /bin/true' --warmup 3
Benchmark 1: urxvt -e /bin/true
Time (mean ± σ): 38.5 ms ± 0.6 ms [User: 31.9 ms, System: 6.5 ms]
Range (min … max): 37.6 ms … 41.4 ms 76 runs
Benchmark 2: urxvtc -e /bin/true
Time (mean ± σ): 12.9 ms ± 1.6 ms [User: 0.4 ms, System: 0.6 ms]
Range (min … max): 9.9 ms … 18.2 ms 195 runs
That’s about 3x faster startup time!
Wrapping up
Not bad, huh? Remember that the only thing we are giving “away” is the fact that if the daemon crashes, all our terminals are gone.
The good news is, I have been using this for a few months and so far it hasn’t crashed a single time.