Life was easy when Firefox was 32-bit and single-process.
In 2021 it is 64 bit, launches bunch of sub-processes, and with many open tabs I routinely see Firefox processes allocate 16Gb of ram and 20Gb of swap slowing down everything else.
I'm using Ubuntu 20.04 and here's what helped me contain unbounded memory usage. Steps for other Linux distros might be similar.
0. Allow tab unloading happen
If you're running Firefox 93 or newer.
- Open
about:config
page. - Find
browser.low_commit_space_threshold_mb
value there. - Set it to some high value, e.g. 2/3 of total memory available in your system.
- Run
free --mebi
. - Find
Mem
/total
value and take approximately 2/3 of it. For example, my machine has32123
MiB total memory, I setbrowser.low_commit_space_threshold_mb
to be24000
.
- Run
What this does?
Short version
This makes tab unloading mechanism always active as opposed to the default setup when tab unloading kicks in only when less than 200 MiB or less than 5% of total memory remains available.
Longer read
Since version ~93, Firefox supports unloading tabs that were not visited recently.
The browser periodically reads /proc/meminfo
and checks MemTotal
and MemAvailable
entries there. Once MemAvailable
drops less than browser.low_commit_space_threshold_percent
(5% by default) of MemTotal
or MemAvailable
falls below browser.low_commit_space_threshold_mb
(200 MiB by default), the tab unloading mechanism is notified to do its job. Links: implementation, ticket.
For me the problem is that Firefox is by far not the only process on the system which needs a lot of memory. The defaults are chosen quite conservatively and don't account for Firefox own contribution to the memory pressure.
Moreover, /proc/meminfo
does not respect cgroups
(more info in this article) which I use below to put limits on swap and memory usage, therefore the browser is unable to understand when its memory usage is approaching the limit I configured for it with cgroup.
Setting browser.low_commit_space_threshold_mb
to aggressively high value should make tab unloading always on. In my experience, this greatly reduces memory footprint except when I open lots of tabs quickly. This also functions like a garbage collector and prevents memory footprint from constantly growing until the browser gets killed.
What's next?
You may stop here and see if forcing tab unloading helps, or you may proceed with further suggestions to prevent Firefox from using swap and put a hard limit on the amount of memory it can use.
1. Enable cgroups v2 and swap control
- Open
/etc/default/grub
for editing (don't forgetsudo
) and findGRUB_CMDLINE_LINUX_DEFAULT
there. Addsystemd.unified_cgroup_hierarchy=1
andswapaccount=1
to it.
Here's how my looks like:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash systemd.unified_cgroup_hierarchy=1 swapaccount=1"
Run
sudo update-grub
.Reboot.
Test if cgroups v2 are enabled by running
mount -t cgroup2
. The output should be similar to this:
$ mount -t cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
What this does?
systemd.unified_cgroup_hierarchy=1
enables cgroups v2 support, that's what will enforce memory limits.
Be aware that once you enable v2, software that uses cgroups v1 (I imagine old Docker versions) may stop working. If you'll run into issues with older software, you know what could be a reason.
swapaccount=1
enables limiting swap usage. If it isn't enabled, cgroups will only enforce memory limits for RAM but not swap and so Firefox processes will not go over RAM limit but will push everything else into swap.
Read here and here if you'd like to learn more about swapaccount
.
2. Edit launcher
I'm using xfce and so I can edit Firefox launch shortcut from the menu. You might do similar depending on how you start the browser.
- The launch command should be:
systemd-run --unit=my-firefox --user --scope -p MemoryHigh=6G -p MemoryMax=6G -p MemorySwapMax=0 firefox %u
- From now use this command/launcher to start the browser.
What this does?
systemd-run
allows you to start a process with ad-hoc settings under control of systemd
and cgroups without having to create so-called unit files. In this case we're starting firefox
but you can apply this to other programs of course.
--unit=my-firefox
gives a name to a systemd "scope" where firefox
process will execute. You can omit this parameter or rename it as you wish. I find it useful to give scope some name to be able to more easily diagnose it later.
--user
tells to run scope in your "user" slice, not in "system". This allows process to be started without asking your password.
--scope
configures how process is started. Frankly I don't know what this does, but you need it.
-p MemoryHigh=6G
sets 6 gigabyte "soft hard" limit on the amount of RAM Firefox processes may allocate. Firefox may request more memory but OS will resist by slowing down allocation.
-p MemoryMax=6G
sets 6 gigabyte "hard" limit on the amount of RAM for Firefox. If it tries to allocate that much, its processes will be killed.
In my experience, if there is a gap between MemoryHigh
and MemoryMax
, the browser becomes slow and almost unusable once it reaches MemoryHigh
. It made little sense to let it run in this mode especially since it does not react on attempts to close window. Therefore, I prefer to have MemoryHigh == MemoryMax
. This way the browser gets killed after reaching the memory limit and you can start it again without having to kill it from a terminal first.
-p MemorySwapMax=0
does not allow Firefox use swap. In my experiments, Firefox performance gets worse if small amount of swap is allowed. If large amounts (>10G) of swap are allowed, Firefox will actively push out lots of memory there and can run overall ok but it wears SSD.
Read about Memory* properties in man systemd.resource-control
.
3. Decrease amount of Firefox processes
Note: this setting seems to be gone in recent versions of Firefox :-(
- Open browser preferences,
about:preferences
- Scroll to "Performance", uncheck "Use recommended performance settings"
- Change "Content process limit" to
2
What this does?
Firefox uses sub-processes to speed up page rendering. The problem is that each sub-process uses lots of RAM. Firefox decides their count automatically and for me the count seems to be 8. Each subprocess using 2Gb of memory easily pushes overall usage to 16Gb or more.
More info at https://support.mozilla.org/en-US/kb/performance-settings.
4. Test and diagnose
Once you started the browser you can see its memory usage with this command
systemctl --user show my-firefox.scope | grep Memory
Here's the output for me showing that the current usage is 5.5Gib.
MemoryCurrent=5596528640
EffectiveMemoryNodes=
AllowedMemoryNodes=
MemoryAccounting=yes
DefaultMemoryLow=0
DefaultMemoryMin=0
MemoryMin=0
MemoryLow=0
MemoryHigh=6442450944
MemoryMax=7516192768
MemorySwapMax=0
MemoryLimit=infinity
- Load many tabs and observe
MemoryCurrent
value. If at some point the browser freezes and you see thatMemoryCurrent
is close to or aboveMemoryHigh
, you may need to tune settings. If things run smoothly all the time, consider decreasingMemoryHigh
.
5. Tune settings
For me Firefox content processes each use 1.5-2.3Gb at peak load, and so MemoryHigh
value should be greater than N*2.3Gb
where N
is the number of content processes you've configured in browser settings.
You also need to include some non-trivial amount for the parent Firefox process and other stuff that it spawns.
If your machine is low on memory, consider decreasing N
, i.e. further decrease amount of content processes in the browser configuration.
Top comments (4)
Your solution breaks xdg-open if firefox is already open:
The reason is clear: xdg-open will simply attempt to create another my-firefox.scope unit by following the instructions in the customized firefox.desktop application, but if firefox is already open then it will cause a resource conflict on that scope.
The solution is to use the following alternative launch command:
This should bypass systemd-run if the scope is already active.
Thanks for the suggestion. YMMV, but I fixed it for myself simply by appending
%u
at the end of the command.In the earlier version of this article it was
it should be
July 2023, Ubuntu 22.04. For some reason, the plain
systemd-run
command stopped working withxdg-open
.I slightly adapted the command from @sankey the following way:
Side note: now giving 16 gigabytes of ram to Firefox. With 6 it got killed way too often.
Unfortunately, this doesn't work for Firefox started as a snap package in a default Ubuntu gnome environment. I would prefer to start Firefox directly but snap gives helpful security isolation for Firefox which I don't want to give up.
Of course you can run snap package using systemd-run but snap creates his own cgroup scope, and systemd-run options doesn't make a sense. I tried different approaches, but didn't find complete solution.
All I've come to is to set cgroup quotas manually for a running process. This script is looking for a cgroup of already started snap-firefox process and set necessary quotas.
A cgroup-tools package must be installed if the lscgroup command is not found.
Now I think on a simple solution how to autostart this script on snap package startup.