# WordPress best practices

A fast, stable WordPress site comes from layered caching, a tuned runtime and disciplined change
management. On TurboStack, much of this is already configured for you when you deploy a `wordpress`
app. Most of your work is enabling the right plugins and avoiding conflicts. This page covers what
the platform configures and the optimizations worth adding on top.

## What TurboStack configures for you

When you deploy WordPress, TurboStack provisions a working, production-shaped stack:

- **Nginx vhost tuned for WordPress** - the document root is `public_html`. Pretty-permalink
  rewrites (`try_files $uri $uri/ /index.php?$args`) are in place. Static assets (`js`, `css`,
  images, `ico`) are served with `expires max`. `xmlrpc.php` is denied to reduce the attack surface.
- **PHP-FPM backend per site** - PHP requests are passed to a dedicated FastCGI pool with a generous
  `fastcgi_read_timeout` (1200s) so long imports and updates do not time out.
- **Varnish full-page cache (optional)** - when `varnish_enabled` is set, a WordPress-aware VCL is
  installed. It bypasses the cache for `wp-admin`, logged-in users, the WooCommerce
  cart/checkout/my-account flows, and `wp-cron.php`. It caches static files for a day, normalises
  query strings, and strips tracking parameters (`utm_*`, `fbclid`, `gclid`). It supports `PURGE`
  from the Proxy Cache Purge plugin.
- **wp-config.php** - generated with your database credentials (`localhost`, dedicated DB and user),
  fresh secret-key salts and `WP_DEBUG` off for production.
- **Optional auto-install** - with `app_install: true`, wp-cli installs core and sets permalinks to
  `/%postname%/`. It removes the default themes and the Akismet/Hello plugins, then installs and
  activates WP Super Cache.

## Recommended optimizations

- **Enable Redis object cache** - turn on Redis as a [service](../../platform/hosts/services.md) and
  install a Redis object-cache plugin (for example `wp-redis`). Repeated queries (options,
  transients, sessions) then hit memory instead of MySQL, which cuts database load - especially for
  WooCommerce and logged-in traffic. Point the plugin at the Redis socket in `wp-config.php`, then
  enable it:
  ```php
  $redis_server = array( 'host' => '/var/run/redis/redis.sock', 'port' => null, 'database' => 1 );
  ```
  ```bash
  wp redis enable
  wp transient delete-all
  ```
  When several sites share one Redis server, give each a different `database` id so they do not
  overwrite each other's cache.
- **Use the Varnish full-page cache** - enable `varnish_enabled` for content-heavy and marketing
  sites; install the Proxy Cache Purge plugin so edits purge the cache automatically. Validate
  compatibility before enabling on stores with heavy logged-in flows.
- **Keep one full-page cache** - run a single full-page caching layer. If Varnish is on, prefer it
  and avoid a second page-cache plugin writing conflicting headers. Without Varnish, a plugin such as
  WP Rocket gives good defaults for mostly-static sites. On an Nginx (non-Varnish) setup, pair WP
  Rocket with the [Rocket-Nginx](https://github.com/SatelliteWP/rocket-nginx) configuration so Nginx
  serves WP Rocket's cached pages directly, bypassing PHP.
- **Consider a few extra speed plugins** - on large sites,
  [Index WP Users for Speed](https://wordpress.org/plugins/index-wp-users-for-speed/) keeps the admin
  fast when there are many users, and [Speed Up Menu](https://wordpress.org/plugins/speed-up-menu/)
  reduces the query overhead of large navigation menus.
- **Index the database** - a plugin such as Index WP MySQL for Speed adds indexes to the core tables
  (`wp_options`, `wp_postmeta`, `wp_posts`, and others), which speeds up slow queries on large sites.
  Install it, add the indexes, then you can remove the plugin again - the indexes stay:
  ```bash
  wp plugin install index-wp-mysql-for-speed
  wp plugin activate index-wp-mysql-for-speed
  wp index-mysql enable wp_commentmeta wp_comments wp_options wp_postmeta wp_posts wp_termmeta wp_usermeta wp_users
  wp plugin deactivate index-wp-mysql-for-speed
  wp plugin uninstall index-wp-mysql-for-speed
  ```
  If it reports that the tables are not found, use your site's actual table prefix instead of `wp_`
  (for example `SsW6eC_commentmeta`, `SsW6eC_options`, and so on).
- **OPcache** - the PHP runtime ships with OPcache; keep it enabled so compiled PHP is reused across
  requests.
- **Optimize assets and images** - use a modern image plugin (WebP, lazy-load) and minify/combine
  CSS and JS to cut requests.
- **Offload heavy media to a Content Delivery Network (CDN)** - front static assets with an HTTP
  cache/CDN to reduce origin load and improve global latency.
- **Speed up Elementor** - if you build pages with Elementor, enable **Element Caching** under
  `Elementor > Settings > Features`. Elementor assembles pages from many queries and template parts
  on every request; caching those lookups cuts database load and speeds up dynamic and logged-in
  views.
- **Raise memory in both places** - if you increase WordPress memory, raise the PHP limit too, or
  the change has no effect: set `WP_MEMORY_LIMIT` / `WP_MAX_MEMORY_LIMIT` in `wp-config.php` *and*
  `memory_limit` in `.user.ini`.

### Set a default TTL for the Redis object cache

By default the Redis object cache does not expire keys that were stored without an explicit
time-to-live (TTL), so the cache can keep growing until it evicts entries or fills memory. The
object-cache drop-in reads a fallback TTL from a constant in `object-cache.php` (in `wp-content`):

```php
if ( ! defined( 'WP_REDIS_DEFAULT_EXPIRE_SECONDS' ) ) {
  define( 'WP_REDIS_DEFAULT_EXPIRE_SECONDS', 0 );
}
```

A value of `0` means "never expire". Set a real fallback - for example `28800` (8 hours) - so keys
stored without their own TTL still age out:

```php
if ( ! defined( 'WP_REDIS_DEFAULT_EXPIRE_SECONDS' ) ) {
  define( 'WP_REDIS_DEFAULT_EXPIRE_SECONDS', 28800 );
}
```

The drop-in talks to the TurboStack Redis cache instance - the socket configured above, or
`localhost` port `6379` with no password. After editing the drop-in, flush the object cache
(`wp cache flush`) so old entries pick up the new default.

This fallback only applies to keys stored without their own lifetime. Code that calls
`wp_cache_set()` with an explicit lifetime keeps that lifetime, and some plugins deliberately store
keys that never expire (for example `posts` keys). To find where such keys are created, search the
codebase for the calls and filter for the group you are chasing:

```bash
grep -Ri "wp_cache_set(" wp-content/plugins | grep 'posts'
```

Then give each of those calls a sensible lifetime instead of leaving it unbounded.

## Run a real cron job

WordPress's built-in `wp-cron.php` runs on page loads, so scheduled tasks fire late on quiet sites
and too often on busy ones, wasting PHP processes. Replace it with a system cron job.

1. Disable the web trigger in `wp-config.php`:
   ```php
   define( 'DISABLE_WP_CRON', true );
   ```
2. Add a cron entry (every 5 minutes) that runs the due events. Wrap the command in `cronlock` so a
   slow run cannot overlap with the next one and pile up PHP processes:
   ```bash
   */5 * * * * cronlock wp cron event run --due-now --path=/var/www/prod/public_html > /dev/null 2>&1
   ```
3. Test it once by hand - it should report the events it executed:
   ```bash
   wp cron event run --due-now --path=/var/www/prod/public_html
   ```

For a multisite network, run the events for every site, not just once:

```bash
wp site list --field=url --path=/var/www/prod/public_html \
  | xargs -n1 -I{} wp cron event run --due-now --path=/var/www/prod/public_html --url="{}"
```

## Plugin hygiene

- **Do not bulk-update every plugin at once.** Blind "update all" is a common cause of broken sites.
  Update selectively, test, and use [Blackfire](../../api/cli.md) to find what actually slows the
  site before changing it.
- **Remove the redundant HTTPS plugin.** TurboStack handles HTTPS and redirects at the web-server
  layer, so the `really-simple-ssl` plugin is not needed:
  ```bash
  wp plugin deactivate really-simple-ssl
  wp plugin uninstall really-simple-ssl
  ```

## Sizing and scaling

Defaults are auto-tuned to the host, so do not pre-emptively raise them. Override sizing variables
only with measured evidence (slow queries, cache evictions, swap):

| Variable | Tune when |
| --- | --- |
| `mysql_innodb_size` | MySQL working set no longer fits in the buffer pool |
| `redis_memory` | The object cache evicts keys under normal load |
| `varnish_cache_size` | Hot pages are being pushed out of the page cache |

See [Performance tuning](../../concepts/performance-tuning.md) for how to measure before you change
anything.

## Stability

- **Back up before every change.** Confirm [backups](../../platform/hosts/backups.md) are running so
  you can restore the database and uploads after a bad plugin or update.
- **Watch [Health](../../platform/hosts/health.md)** for CPU, memory, PHP-FPM and database pressure,
  and act on sustained trends.
- **Keep WordPress, plugins, themes and PHP current** - most outages and exploits trace back to
  stale code; apply security updates promptly.
- **Test on a staging clone first.** Trial core upgrades, major plugin changes and theme switches on
  a copy before publishing to production.

## Related

- [Deploy WordPress](deploy.md)
- [WordPress reference](reference.md)
- [Troubleshooting WordPress](troubleshooting.md)
- [Services](../../platform/hosts/services.md)
- [Performance tuning](../../concepts/performance-tuning.md)
