Reducing Worker Memory Usage by 30-50%

Reducing Worker Memory Usage by 30-50%

TL;DR: OSS Password Pusher v2.12.0 reduces worker memory usage by 30-50% by switching SolidQueue’s supervisor mode from fork to async. The web process is completely unaffected. For most self-hosted deployments, updating the container image is all that’s needed. Thanks to @gseilheimer for the research that made this happen.

The Background

The Password Pusher Docker container runs two main components: a web process (Puma) that serves your requests, and a worker process (SolidQueue) that handles background jobs — expiring pushes, cleaning up finished jobs, and other recurring maintenance tasks.

SolidQueue ships with two supervisor modes: fork and async. Fork mode has been the default, and it’s what the Docker image has used since SolidQueue was adopted.

In fork mode, SolidQueue spawns four separate OS processes: a supervisor, a dispatcher, a worker, and a scheduler. Each process loads the full Rails application into memory independently. For a set of lightweight, infrequent background jobs, that’s a lot of overhead.

The Research

Community member @gseilheimer profiled their self-hosted instance using PSS (Proportional Set Size) from /proc/smaps_rollup and filed the results in issue #4773. The numbers made the problem clear:

Process Memory (PSS)
Puma (web) 169 MB
SolidQueue supervisor 81 MB
SolidQueue dispatcher 77 MB
SolidQueue worker 80 MB
SolidQueue scheduler 77 MB
Foreman + Thruster 30 MB
Total 516 MB

SolidQueue’s four processes accounted for 315 MB — 61% of total container memory — for a handful of recurring maintenance jobs. The web process, the part that actually serves your pushes and requests, was only 169 MB.

@gseilheimer also tested switching to async mode (SOLID_QUEUE_SUPERVISOR_MODE=async), which collapses those four processes into a single multi-threaded process. They verified that all six recurring maintenance jobs continued to execute correctly and that all four SolidQueue roles maintained healthy heartbeats. This picked up a thread I’d left open in issue #3720 about eventually investigating async mode.

The Result

PR #4816 makes async the default supervisor mode for the Docker image. The before-and-after for the worker process:

Metric Fork Mode Async Mode Change
SolidQueue (worker) PSS 315 MB 143 MB -55%
Application memory (anon) 487 MB 318 MB -35%
Puma (web) PSS 169 MB 173 MB unchanged

All the savings come from the worker infrastructure. The web process is completely unaffected — it continues to serve requests exactly as before.

What This Means for Self-Hosted Deployments

If you’re running Password Pusher via Docker, updating to v2.12.0 gives you these savings automatically. No configuration changes needed.

If you’ve explicitly set SOLID_QUEUE_SUPERVISOR_MODE=fork in your environment, that will still be respected. The change only affects the default.

For instances running on constrained hardware — Raspberry Pis, small VPSes, shared containers — reclaiming that worker memory is meaningful. It also makes Password Pusher a better neighbor in multi-container environments where every megabyte of headroom counts.

When Fork Mode Still Makes Sense

Async mode uses threads instead of processes for the worker. For the vast majority of Password Pusher deployments, this is fine — the background jobs are lightweight and infrequent. But if you’re running a high-volume instance where job throughput and process isolation matter, fork mode still exists as an option. Set SOLID_QUEUE_SUPERVISOR_MODE=fork and you’re back to the previous behavior.

Upgrading

Pull the latest image and restart:

docker pull pglombardo/pwpush:latest

That’s it. The v2.12.0 release notes have the full details.

Thanks to @gseilheimer for doing the legwork on this one — profiling the memory usage, testing the fix, and verifying the results. This is exactly the kind of contribution that makes open source work.


Peter Giacomo Lombardo Founder & Principal, Apnotic · Creators of Password Pusher