How to Optimize WordPress on a 1 GB RAM Server

By SiteHarbour Team· ·Updated ·15 min read

Yes, WordPress can run well on a server with 1 GB of RAM, as long as you tune the whole stack instead of a single setting, and make sure most visitors are served from a page cache rather than by PHP. The changes below are ordered by impact: cache pages, cap PHP-FPM workers, size MariaDB to your real data, enable OPcache, trim plugins, and add swap as a safety net.

The examples use Ubuntu 24.04 with Nginx, PHP 8.3 and MariaDB (MySQL works the same way; only paths and service names differ). Replace 8.3 with your PHP version and adjust paths for your distribution. Every step starts with measurement, because the right numbers depend on your plugins, theme and traffic, not on a universal template.

Where the memory actually goes

On a 1 GB server, RAM is shared by the operating system, Nginx, PHP-FPM workers, MariaDB and anything else you run, such as a mail daemon, a monitoring agent or a second site. Two components decide whether the server stays stable:

  • PHP-FPM workers. Each concurrent PHP request needs its own worker process. A lean WordPress worker can use tens of megabytes; a site with a page builder, WooCommerce or many plugins can use well over 100 MB per worker. Total PHP memory is the number of workers multiplied by their average size.
  • MariaDB or MySQL. Memory is dominated by the InnoDB buffer pool plus per-connection buffers. Default settings are often sized for much larger machines.

As a rough planning sketch, not a benchmark: reserve roughly 150 to 250 MB for the operating system and background services, 30 to 50 MB for Nginx, and 150 to 250 MB for MariaDB. That leaves somewhere around 350 to 500 MB for PHP-FPM. Your real figures will differ, which is why the first step is to measure.

Step 1: Measure before you change anything

Start with a snapshot of overall memory and the biggest consumers:

free -h
ps -eo pid,rss,cmd --sort=-rss | head -15

The rss column is in kilobytes. To see how large your PHP-FPM workers really are, average them:

ps -C php-fpm8.3 -o rss= | awk '{s+=$1; n++} END {printf "processes: %d, average: %.0f MB\n", n, s/n/1024}'

This includes the small master process and counts shared memory more than once, so treat it as an upper estimate. For a fairer figure, install smem and read the proportional set size (PSS) column:

sudo apt install smem
sudo smem -t -k -P php-fpm

Then check whether the server is already under memory pressure. Watch the si and so columns of vmstat; values that stay above zero mean the system is actively swapping. Also look for out-of-memory kills:

vmstat 1 5
sudo journalctl -k | grep -i "out of memory"

Write these numbers down. They are your baseline for judging every change that follows.

Step 2: Serve pages from a cache so PHP barely runs

The single biggest improvement on a small server is not running PHP for anonymous visitors at all. Nginx FastCGI caching stores the finished HTML and serves it directly, so most requests never reach WordPress or the database.

First define the cache zone in the http context, for example in /etc/nginx/conf.d/wp-cache.conf:

fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=WPCACHE:10m max_size=256m inactive=60m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

Then, in your site’s server block, decide which requests must never be cached, and attach the cache to the PHP location:

set $skip_cache 0;
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|wp-.*\.php|/feed/") { set $skip_cache 1; }
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") { set $skip_cache 1; }

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_cache WPCACHE;
    fastcgi_cache_valid 200 301 302 30m;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache_lock on;
    fastcgi_cache_use_stale error timeout updating http_500 http_503;
    add_header X-FastCGI-Cache $upstream_cache_status;
}

Create the directory, test the configuration and reload:

sudo mkdir -p /var/cache/nginx/wordpress
sudo chown www-data:www-data /var/cache/nginx/wordpress
sudo nginx -t && sudo systemctl reload nginx

Verify it works by requesting the same page twice. The first response should say MISS and the second HIT:

curl -sI https://example.com/ | grep -i x-fastcgi-cache
curl -sI https://example.com/ | grep -i x-fastcgi-cache

Two things to know. Requests with a query string bypass the cache in this setup, so links with tracking parameters are served dynamically. And new or edited content appears when the cached copy expires, or when you clear the cache directory. If you always see BYPASS or MISS, look for a plugin that sends a Set-Cookie or Cache-Control: no-cache header on public pages. If you cannot change the Nginx configuration, a lightweight page-cache plugin is the fallback, but the server-level cache uses less memory.

Step 3: Right-size PHP-FPM

The pool configuration decides how many PHP workers can exist at once. Too many and the server runs out of memory; too few and requests queue. The three process managers behave differently:

Mode Behaviour Best for
static A fixed number of workers is always running Busy, predictable servers with plenty of RAM
dynamic Keeps a pool between minimum and maximum spare workers Steady traffic where consistent latency matters
ondemand Starts workers when requests arrive and stops idle ones Small servers with low or bursty traffic

On a 1 GB server with a page cache in front, ondemand is usually a sensible starting point because idle workers use no memory. The trade-off is a small delay when a worker has to start after a quiet period.

To choose pm.max_children, divide the memory you can give PHP by the average worker size you measured. As a worked example only: if PHP can use about 400 MB and workers average 55 MB, then 400 ÷ 55 is about 7, so starting at 6 leaves headroom. Edit /etc/php/8.3/fpm/pool.d/www.conf:

pm = ondemand
pm.max_children = 6
pm.process_idle_timeout = 20s
pm.max_requests = 500

pm.max_requests recycles each worker after that many requests, which contains slow memory growth from leaky code. Keep memory_limit in mind as well: it is a per-request ceiling, not the typical size of a worker, so a value such as 128M is a reasonable default, and only raise it if a specific admin task needs more.

Test and reload, then watch the PHP-FPM log for the warning that means your limit is too low:

sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm
sudo grep -i "max_children" /var/log/php8.3-fpm.log

If you see “server reached pm.max_children” repeatedly during normal traffic, the fix is a faster site or more RAM, not blindly raising the number until the server starts swapping.

For a full walkthrough of measuring worker size and calculating this limit, see how to optimize PHP-FPM workers for limited RAM.

Step 4: Enable and size OPcache

OPcache keeps compiled PHP scripts in shared memory so WordPress does not recompile its files on every request. It saves CPU and speeds up every uncached page. On a small server, a modest allocation is enough. In /etc/php/8.3/fpm/conf.d/10-opcache.ini:

opcache.enable=1
opcache.memory_consumption=96
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1
opcache.revalidate_freq=60

With validate_timestamps=0, PHP never checks whether files changed. That is faster, but it means you must reload PHP-FPM after every plugin, theme or core update, or the old code keeps running. If you are not automating that reload, leave timestamp validation on.

Step 5: Cap MariaDB or MySQL memory

First check how big your WordPress data really is, because the buffer pool only needs to hold the data you actually use. Replace wordpress with your database name:

sudo mysql -e "SELECT ROUND(SUM(data_length+index_length)/1024/1024,1) AS size_mb FROM information_schema.tables WHERE table_schema='wordpress';"

Instead of editing the packaged file, create your own override in /etc/mysql/mariadb.conf.d/99-lowram.cnf (MariaDB) or /etc/mysql/mysql.conf.d/zz-lowram.cnf (MySQL; the name makes it load after the packaged mysqld.cnf) so package updates do not overwrite it:

[mysqld]
innodb_buffer_pool_size = 128M
max_connections = 40
tmp_table_size = 32M
max_heap_table_size = 32M

For many small WordPress sites, 128 to 256 MB is plenty for the buffer pool; go higher only if the database is larger and you have measured memory to spare. Forty connections is generous when PHP is limited to a handful of workers. Restart the service and confirm the values took effect:

sudo systemctl restart mariadb   # on MySQL: sudo systemctl restart mysql
sudo mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
ps -C mariadbd,mysqld -o comm=,rss=

Leave durability settings such as innodb_flush_log_at_trx_commit alone. Trading safety for a little speed is not worth it on a production site.

Step 6: Small Nginx wins for static files

Nginx itself uses little memory, but letting browsers cache static files reduces the number of requests your server handles at all. Add compression and long expiry headers for assets:

gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;

location ~* \.(?:css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2)$ {
    expires 30d;
    add_header Cache-Control "public";
    access_log off;
}

Also compress and resize images before uploading them. Serving a 3 MB photo that displays at 800 pixels wide wastes bandwidth and slows every page that uses it.

Step 7: Find and remove plugin overhead

Plugins are the most common reason a WordPress worker grows large. Start by listing what is active and removing anything you do not use:

wp plugin list --status=active

To see where time and database queries go, install WP-CLI’s profiling package and inspect the stages of a page load:

wp package install wp-cli/profile-command
wp profile stage --spotlight

The result shows whether the cost sits in bootstrap (plugins loading), the main query, or the template. For memory, a practical method is to deactivate a suspect plugin on a staging copy, restart PHP-FPM, warm the site with some requests and compare the worker size again. Be cautious with all-in-one security suites, backup plugins that run inside WordPress, page builders and anything that runs scheduled scans; on a small server, server-level backups and a firewall usually do the same job more cheaply.

Step 8: WP-Cron and autoloaded options

By default, WordPress runs scheduled tasks when visitors load pages. On a cached site that is unreliable, and a slow task can tie up a worker. Move it to a real system job. In wp-config.php add:

define('DISABLE_WP_CRON', true);

Then run it from the web user’s crontab (sudo crontab -u www-data -e), adjusting the path:

*/5 * * * * cd /var/www/example && /usr/bin/php wp-cron.php >/dev/null 2>&1

Next, check autoloaded options. WordPress loads every option flagged for autoload on every single request, so a bloated set slows every uncached page and inflates memory. Site Health starts warning at roughly 800 KB. Check the total, and list the largest entries (replace wp_ with your table prefix):

wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024) AS autoload_kb FROM wp_options WHERE autoload IN ('yes','on','auto-on','auto');"
wp db query "SELECT option_name, LENGTH(option_value) AS bytes FROM wp_options WHERE autoload IN ('yes','on','auto-on','auto') ORDER BY bytes DESC LIMIT 10;"

Large entries often belong to plugins you removed long ago. Research an option before changing it, and take a database backup first. Also clear expired transients periodically and cap stored revisions:

wp transient delete --expired
# in wp-config.php
define('WP_POST_REVISIONS', 5);

Step 9: Add swap as a safety net, not as extra RAM

Swap will not make a struggling server fast, but a small swap file can turn a sudden memory spike into a brief slowdown instead of an out-of-memory kill. Create a 1 GB file and keep the system reluctant to use it:

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system
swapon --show

Swap on network-backed cloud disks is slow, so it should be used rarely. If vmstat shows constant swap activity under ordinary traffic, the swap file is hiding a real capacity problem.

Step 10: Make failures recoverable

Even a well-tuned small server should restart its services automatically if one is killed. Add a systemd override for PHP-FPM:

sudo systemctl edit php8.3-fpm

and enter:

[Service]
Restart=on-failure
RestartSec=5s

Combine this with an external uptime monitor so you learn about downtime from an alert instead of from a visitor, and re-run the measurements from Step 1 after each change so you can see what actually helped.

What this looked like on a real 1 GB server

Numbers make Step 1 concrete, so here is a real snapshot. It comes from a production AWS EC2 server running Ubuntu 24.04 with 911 MiB of usable RAM and 2 GB of swap, with Nginx, PHP 8.4-FPM and MySQL 8.0. The same Nginx and PHP-FPM pool serve a Laravel application and a WordPress blog. It is one server at one quiet moment, so treat it as an example of what to look for, not as numbers to copy.

$ free -mh
               total        used        free      shared  buff/cache   available
Mem:           911Mi       664Mi       102Mi        71Mi       386Mi       247Mi
Swap:          2.0Gi        71Mi       1.9Gi

Only 102 MiB is “free”, which looks alarming, but 386 MiB is cache the kernel hands back when programs need it. The available column, 247 MiB, is the honest measure of headroom. About 71 MiB has been moved to swap, so the next question is whether the server is swapping right now. vmstat answers it, with samples taken one second apart:

$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 0  0  74260 139220   9288 370080    0    0     0     0  385  616  0  1 99  0  0  0
 0  0  74260 142420   9320 370080    0    0     0   120  588  987  1  1 98  1  0  0

The si and so columns (swap-in and swap-out) are zero, so those 71 MiB are old pages parked in swap, not active swapping. The CPU was about 99 percent idle at the time.

The biggest memory users, from ps -eo pid,rss,cmd --sort=-rss, in resident memory (RSS), rounded to megabytes:

Process Resident memory (RSS)
Database server (mysqld) about 188 MB
PHP-FPM workers (three running) 92 to 98 MB each
PHP-FPM master process about 40 MB
fail2ban about 34 MB
multipathd about 25 MB
snapd about 15 MB
fwupd about 12 MB
amazon-ssm-agent about 11 MB
Nginx worker about 10 MB

Why RSS misleads: RSS versus PSS

Those PHP-FPM figures look alarming, but RSS counts every shared page, such as the OPcache segment and shared libraries, in full for every worker. Proportional set size (PSS) divides each shared page between the processes that use it, so it is the fairer number. You can read it without installing anything:

for p in $(pgrep -f 'php-fpm: pool www'); do echo -n "$p "; sudo grep '^Pss:' /proc/$p/smaps_rollup; done

On this server it printed:

1537590 Pss:               31918 kB
1537591 Pss:               35464 kB
1537595 Pss:               33173 kB

That is about 31 to 35 MB per worker, roughly a third of what RSS suggested, for the same processes at the same moment. The whole difference is shared memory being counted more than once.

This changes the sizing arithmetic. Suppose you can give PHP 300 MB. Dividing by RSS gives about 3 workers (300 ÷ 95); dividing by PSS gives about 9 (300 ÷ 33). PSS is closer to the truth, but it is still a snapshot: workers grow while handling heavy requests, so leave a margin and check under real load. This pool is configured with:

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500

Five workers at the measured PSS come to about 165 MB, which is comfortable here. A cap like this is also useful for protecting the rest of the machine, because it stops a burst of slow requests from starting more workers than the server can afford.

What else the snapshot shows

  • The database is the largest single process, at roughly a fifth of all RAM in RSS terms.
  • A stock cloud image runs services you may not need. multipathd, snapd and fwupd use roughly 50 MB together (RSS, so an upper bound). fail2ban is worth its memory, and the SSM agent is required if you use AWS Systems Manager, so check what depends on a service before disabling it.

Your numbers will differ, which is why measuring, and measuring the right thing, comes before any tuning.

When 1 GB stops being enough

Tuning has limits. These signs mean the server is undersized for the workload:

  • Swap stays in use during normal traffic, with constant si and so activity in vmstat.
  • The kernel log shows out-of-memory kills of MariaDB or PHP-FPM.
  • The pm.max_children warning keeps returning even after the site has been slimmed down.
  • Much of your traffic cannot be cached, as with WooCommerce carts, membership sites or many logged-in users.
  • You are running several sites on the same 1 GB machine.

At that point the next step is 2 GB or more of RAM, moving the database to its own server, or moving to managed WordPress hosting where the stack is already tuned. For sites with sustained high traffic, a dedicated server removes the shared-resource limits entirely.

A practical order of operations

  1. Measure memory, worker size and swap activity, and write the numbers down.
  2. Put an Nginx FastCGI cache in front of PHP.
  3. Set PHP-FPM to ondemand and calculate pm.max_children from your measured worker size.
  4. Confirm OPcache is enabled and decide how you handle reloads after updates.
  5. Cap MariaDB or MySQL with a buffer pool sized to your real data.
  6. Remove or replace heavy plugins.
  7. Move WP-Cron to a system job and trim autoloaded options.
  8. Add a small swap file and automatic service restarts.
  9. Re-measure, and repeat only where the numbers show a problem.

Frequently asked questions

How many WordPress sites can one 1 GB server run?

Usually one, or two small and well-cached sites. The answer depends on your measured worker size and traffic, so use the calculation in Step 3 instead of a fixed rule.

Is Redis worth it on 1 GB?

Only if many requests miss the page cache, such as logged-in users or WooCommerce. An object cache stores database results in RAM, so cap it (for example maxmemory 64mb with maxmemory-policy allkeys-lru) and re-measure. For a mostly public blog, the page cache already does most of the work.

Should I use Nginx or Apache?

Nginx with PHP-FPM generally uses less memory under concurrent load than Apache running PHP inside its worker processes. Apache with the event MPM and PHP-FPM can perform comparably, so the bigger factor is how the whole stack is configured.

Should I choose ondemand or dynamic?

Choose ondemand for low or bursty traffic on a tiny server, and dynamic if traffic is steady and you prefer consistent response times over the lowest idle memory.

A 1 GB server is a constraint, not a dead end. Measure first, cache aggressively, cap each component with numbers from your own workload, and you can run a fast, stable WordPress site on very modest hardware.