Step 7: Rate limiting and connection limits
WordPress login pages, search results, and uncached PHP endpoints are targets for brute-force and flood attacks. Rate limit them:
# In nginx.conf http { } block
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/m;
limit_conn_zone $binary_remote_addr zone=phplimit:10m;
# In your WordPress server block
location = /wp-login.php {
limit_req zone=login burst=2 nodelay;
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
location ~ \.php$ {
limit_conn phplimit 5;
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
The login page gets one request per minute with a burst of two (for legitimate password errors and retries). The PHP handler caps any single IP to five concurrent PHP requests — enough for a browser loading multiple uncached pages, not enough for an attacker to exhaust your PHP-FPM pool.
These examples use $binary_remote_addr (the visitor's IP as Nginx sees it). If your site runs behind Cloudflare or another reverse proxy, you'll want to restore real visitor IPs first — otherwise you're rate-limiting Cloudflare's IPs instead of your actual visitors. See Restoring Real Visitor IPs with Cloudflare and Nginx for the complete setup.
Step 8: wp-config.php essentials
A few lines in wp-config.php that make a difference in production:
// Disable the built-in cron — use a system cron job instead.
// WordPress's default cron fires on every page load, which adds
// latency for visitors and misses scheduled tasks on low-traffic sites.
define('DISABLE_WP_CRON', true);
Add a system cron job to hit wp-cron.php every 10 minutes (or as often as your site needs):
# /etc/cron.d/wordpress — or add to your user's crontab
*/10 * * * * www-data curl -s --resolve mysite.com:443:127.0.0.1 \
https://mysite.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1
Using curl with --resolve sends the request through your site's local Nginx → PHP-FPM stack at full speed, without leaving the server. It's faster than an external HTTP request and works even if you don't have DNS resolution pointing to the server yet.
// Increase memory limits if your default PHP pool is conservative
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '256M');
// Debugging — off in production (this is the default, but be explicit)
define('WP_DEBUG', false);
Step 9: Redirect www and HTTP to HTTPS
# Redirect www to non-www
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.mysite.com;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
return 301 https://mysite.com$request_uri;
}
# Redirect plain HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name mysite.com www.mysite.com;
return 301 https://mysite.com$request_uri;
}