Apache for WordPress: Modules and Rules That Break Sites (and How to Fix Them)

Was this helpful?

Most WordPress outages on Apache don’t start with WordPress. They start with “one small Apache change” that looked harmless:
a new security rule, a compression tweak, a redirect “cleanup,” or a clever caching header copied from a blog written in 2014.
Then your homepage redirects to itself, the admin turns into a 403 museum exhibit, or REST API calls mysteriously fail only on Tuesdays.

This is a field guide for production systems: which Apache modules and config patterns commonly break WordPress, how to prove that’s what’s happening,
and how to fix it without flailing. If you operate WordPress at scale, this is less “web server basics” and more “stop the bleeding, then prevent it.”

Fast diagnosis playbook

When WordPress “breaks” on Apache, the fastest path is to classify the failure by HTTP symptom,
then confirm the responsible layer (Apache vs PHP-FPM vs WordPress vs network/CDN).
Don’t guess. Make the server tell you.

First: Identify the failure class in 90 seconds

  1. Get a clean response from the origin (bypass CDN if present). Use curl with verbose and follow redirects.
    You’re looking for status code patterns and redirect chains.
  2. Check Apache error logs for the same timestamp. A single line often names the culprit module (security2, rewrite, proxy_fcgi).
  3. Check access log fields: status, bytes, request time, upstream time (if logged), user agent.
    If you don’t log request time, you should—today.

Second: Decide which of these four buckets you’re in

  • Redirect loop / wrong scheme / wrong host: almost always rewrite rules, proxy headers, or WordPress siteurl/home mismatch.
  • 403 Forbidden: mod_security, filesystem permissions, or Apache authz rules (Require, AllowOverride, Options).
  • 500/502/503: PHP handler miswire (proxy_fcgi), MPM/PHP mismatch, timeouts, or memory limits.
  • Slow or flaky: KeepAlive, HTTP/2 edge cases, compression, caching headers, backend saturation, disk IO, or DNS/proxy issues.

Third: Confirm with one targeted test

Pick the test that collapses uncertainty:
disable a module on a staging vhost, bypass rewrite for one path, run a single request with RewriteLog-level instrumentation,
or replay one request against the backend directly.

One operational quote worth tattooing on your runbook, because it stays true under pressure:
“Hope is not a strategy.” — Gene Kranz

Interesting facts and historical context (useful, not trivia)

  • .htaccess exists largely for shared hosting: it let users control per-directory config without root access. It’s convenient and expensive at runtime.
  • mod_php pushed Apache toward prefork: for years, running PHP inside Apache meant prefork MPM was the safe default. Many WordPress hosts still carry that legacy.
  • WordPress “pretty permalinks” are basically rewrite rules: the canonical rules were written for mod_rewrite first, then adapted elsewhere. Apache is the reference shape.
  • mod_security became mainstream in the 2000s: it’s powerful, and its false positives against WordPress admin and XML-RPC are legendary.
  • HTTP/2 in Apache matured over time: early deployments hit compatibility edges with certain clients and intermediaries. It’s mostly solid now, but misconfigurations still bite.
  • Brotli support arrived later than gzip: mod_brotli is newer, and old proxies/caches can behave badly if Vary headers aren’t right.
  • Apache’s authz syntax changed: the old “Order allow,deny” era gave way to “Require all granted.” Mixing versions or cargo-culting snippets causes accidental 403s.
  • “AllowOverride None” was the performance PSA: disabling .htaccess scanning improves performance, but it will break WordPress permalinks unless you move rules into vhost config.

The usual site-breakers: modules and rules that hurt WordPress

1) mod_rewrite: the silent author of your redirect loops

WordPress needs rewrite rules for pretty permalinks. The problem isn’t mod_rewrite itself; it’s humans.
Specifically: humans mixing redirects (301/302), canonical host enforcement, HTTP→HTTPS, and trailing slash cleanup
across multiple layers (Apache + WordPress + CDN + load balancer).

Typical breakage patterns:

  • Infinite redirect to the same URL: two rules disagree about scheme or host, or WordPress thinks it’s on HTTP while clients arrive via HTTPS.
  • Admin redirects to homepage: WordPress sees a different host/scheme than the browser due to missing proxy headers.
  • REST API 404: rewrite rules not applied to /wp-json because AllowOverride blocks .htaccess, or because a rule short-circuits.

Practical opinion: if you have root access, keep WordPress rewrite rules out of .htaccess and in the vhost. Fewer filesystem stats, fewer surprises.
But if you do that, treat it like code: version it, test it, and document the invariants (hostnames, schemes, and canonical paths).

2) AllowOverride and Options: “works on staging” is not a configuration

The single most common “WordPress permalinks broke” cause on Apache is not WordPress at all. It’s Apache ignoring the rules.
That happens when you set:

  • AllowOverride None (so .htaccess is ignored)
  • or you override Options / Require in a way that blocks access.

The “good” fix is to either allow the specific override types WordPress needs (usually AllowOverride FileInfo at minimum),
or move the rewrite rules into the vhost and keep AllowOverride None for performance.

3) mod_security (security2): protection that frequently blocks the admin

mod_security is a web application firewall. It inspects requests and blocks patterns that look malicious.
WordPress admin traffic looks malicious on a good day: lots of parameters, serialized data, JSON blobs, and plugins with “creative” query strings.

Classic failure modes:

  • 403 on wp-admin after installing a plugin or enabling a page builder.
  • REST API requests blocked, causing block editor failures, broken embeds, or AJAX errors.
  • Random logouts because certain cookies/headers trigger rules.

Opinionated guidance: don’t disable mod_security globally. Tune it per vhost and per rule ID, and keep an audit log you actually read.
“We turned off the WAF” is a short-term fix that becomes a long-term incident report.

4) mod_proxy_fcgi and PHP-FPM: 502s that look like WordPress but aren’t

Modern Apache + WordPress often means Apache serves static assets and proxies PHP to PHP-FPM via proxy_fcgi.
Misconfigurations here produce:

  • 502 Bad Gateway (FPM down, socket permissions, wrong path, timeout)
  • 503 Service Unavailable (backend saturated, max children reached)
  • Intermittent 500s (crashes, memory exhaustion, slow requests hitting timeouts)

5) MPM choice (event/worker/prefork): performance tuning that breaks under load

Apache’s Multi-Processing Module (MPM) decides how it handles connections.
WordPress itself doesn’t care, but your PHP integration and traffic shape do.

  • prefork: old-school process-per-connection model; works with mod_php; memory heavy.
  • worker: threads; better concurrency; requires thread-safe module behavior.
  • event: best general-purpose for keep-alive heavy traffic; pairs well with PHP-FPM.

Breakage comes from mismatch: enabling event MPM but still loading mod_php, or setting aggressive MaxRequestWorkers without memory headroom.
WordPress becomes the scapegoat while Apache is quietly OOM-killing workers.

6) HTTP/2 (mod_http2): great until proxies and headers are wrong

HTTP/2 usually helps WordPress frontends by multiplexing requests. But misconfigurations cause strange failures:

  • Some clients hang due to intermediate proxies or buggy TLS settings.
  • Sudden CPU increase with certain cipher suites and compression settings.
  • Push (if used) can make caches grumpy and waste bandwidth. Most sites should not do HTTP/2 server push anymore.

7) Compression modules: mod_deflate vs mod_brotli and the Vary header trap

Compression saves bandwidth. It can also create cache poison if your caching layer doesn’t respect content negotiation.
If you enable brotli and gzip, you must get the response headers right, especially Vary: Accept-Encoding.

A common self-own: enabling brotli for everything, then discovering an upstream cache serves brotli-compressed CSS to a client that doesn’t support it.
The page becomes a modern art exhibit of broken styles.

8) Caching headers and mod_expires: “optimization” that freezes your theme in time

You want long cache lifetimes for static assets. You do not want long cache lifetimes for HTML or for dynamic endpoints.
WordPress themes and plugins ship assets that change; if you set caching too aggressively without versioned filenames, users keep old JS forever.

9) mod_headers: security headers that break login flows

Security headers matter. But you can break WordPress authentication and embeds if you get overzealous:

  • SameSite cookies with odd settings can break third-party integrations.
  • Content-Security-Policy too strict breaks the block editor, embeds, and analytics.
  • X-Frame-Options blocks legitimate iframe use cases (previews, some SSO flows) if you don’t plan exceptions.

10) DirectoryIndex, MultiViews, and other “small” features that cause big weirdness

Apache features that look unrelated to WordPress frequently collide with it:

  • MultiViews (content negotiation): can break permalinks by mapping URLs to unexpected files. If you run WordPress, you usually want it off.
  • DirectoryIndex misconfigurations: can cause 403/404 where you expected WordPress front controller behavior.
  • Alias and ProxyPass collisions: a single conflicting path can blackhole /wp-admin or /wp-json.

Joke #1: Apache can do almost anything. That’s also the problem—so can your coworkers.

Practical tasks: commands, outputs, and decisions

Below are concrete tasks you can run on an Apache host. Each one includes what the output means and the decision you should make.
These aren’t “nice-to-haves.” They’re the shortest path from symptoms to cause.

Task 1: Confirm the real failure mode from the origin with curl

cr0x@server:~$ curl -svL -o /dev/null https://example.com/ 2>&1 | sed -n '1,25p'
*   Trying 203.0.113.10:443...
* Connected to example.com (203.0.113.10) port 443 (#0)
> GET / HTTP/2
> host: example.com
> user-agent: curl/7.81.0
> accept: */*
< HTTP/2 301
< location: http://example.com/
< server: Apache
* Issue another request to this URL: 'http://example.com/'
> GET / HTTP/1.1
> Host: example.com
< HTTP/1.1 301 Moved Permanently
< Location: https://example.com/

What it means: You have a scheme-flip loop: HTTPS redirects to HTTP, then HTTP redirects back to HTTPS.
That’s usually two different redirect sources fighting (Apache rewrite plus app-level canonicalization).

Decision: Find and remove one side. Prefer: Apache enforces HTTPS; WordPress should be configured to believe it’s behind HTTPS (proxy headers).

Task 2: See which Apache MPM is active

cr0x@server:~$ apachectl -V | egrep -i 'server mpm|httpd_root|server_config_file'
Server MPM:     event
 -D HTTPD_ROOT="/etc/apache2"
 -D SERVER_CONFIG_FILE="apache2.conf"

What it means: You’re on event MPM. That’s good for concurrency, but you must not be using mod_php.

Decision: Ensure PHP runs via PHP-FPM (proxy_fcgi). If you see prefork, verify memory headroom and worker counts.

Task 3: List loaded modules (look for the usual suspects)

cr0x@server:~$ apachectl -M | egrep -i 'rewrite|security2|http2|deflate|brotli|headers|proxy_fcgi|mpm'
 headers_module (shared)
 http2_module (shared)
 proxy_fcgi_module (shared)
 rewrite_module (shared)
 security2_module (shared)
 deflate_module (shared)
 mpm_event_module (shared)

What it means: rewrite and security2 are active, plus HTTP/2 and compression. Any of these can explain a regression.

Decision: If you have 403s: focus on security2 and authz. If you have redirects: focus on rewrite and proxy headers. If you have weird client hangs: inspect http2/TLS.

Task 4: Confirm whether .htaccess is being honored

cr0x@server:~$ apachectl -S 2>/dev/null | sed -n '1,40p'
VirtualHost configuration:
*:80                   example.com (/etc/apache2/sites-enabled/example.conf:1)
*:443                  example.com (/etc/apache2/sites-enabled/example-ssl.conf:1)
ServerRoot: "/etc/apache2"
Main DocumentRoot: "/var/www/html"

This doesn’t show AllowOverride directly, so check the vhost file.

cr0x@server:~$ sudo apachectl -t -D DUMP_INCLUDES | sed -n '1,120p'
Included configuration files:
  (/etc/apache2/apache2.conf:215)
  (/etc/apache2/sites-enabled/example.conf:1)
  (/etc/apache2/sites-enabled/example-ssl.conf:1)

Now inspect the directory stanza:

cr0x@server:~$ sudo sed -n '1,140p' /etc/apache2/sites-enabled/example.conf
<VirtualHost *:80>
  ServerName example.com
  DocumentRoot /var/www/example.com/public

  <Directory /var/www/example.com/public>
    Options FollowSymLinks
    AllowOverride None
    Require all granted
  </Directory>
</VirtualHost>

What it means: .htaccess is ignored. WordPress rewrite rules in .htaccess won’t apply. Permalinks will 404 or behave oddly.

Decision: Either set AllowOverride FileInfo (or All, if you enjoy audits), or move WordPress rewrite rules into the vhost.

Task 5: Validate the WordPress rewrite rules exist where you think they do

cr0x@server:~$ sudo grep -n "BEGIN WordPress" -n /var/www/example.com/public/.htaccess
2:# BEGIN WordPress

What it means: The rules exist in .htaccess, but Task 4 shows Apache ignores them.

Decision: Don’t “re-save permalinks” in wp-admin and pretend it fixed anything. Fix Apache first.

Task 6: Check for MultiViews (it breaks “pretty URLs” in fun ways)

cr0x@server:~$ sudo apachectl -t -D DUMP_RUN_CFG | egrep -i 'multiviews|options'

What it means: This dump isn’t always verbose about Options at directory level.

Decision: Grep your config for MultiViews and disable it for WordPress docroots.

cr0x@server:~$ sudo grep -RIn "MultiViews" /etc/apache2 | head
/etc/apache2/conf-enabled/negotiation.conf:12:Options +MultiViews

Decision: Change it to Options -MultiViews in the WordPress vhost or directory stanza.

Task 7: Confirm the PHP handler wiring (FPM socket and permissions)

cr0x@server:~$ sudo apachectl -t -D DUMP_MODULES | grep -E 'proxy_fcgi|php'
 proxy_fcgi_module (shared)
cr0x@server:~$ sudo grep -RIn "SetHandler.*proxy:unix" /etc/apache2/sites-enabled/example-ssl.conf
38:  <FilesMatch \.php$>
39:    SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost/"
40:  </FilesMatch>
cr0x@server:~$ ls -l /run/php/php8.2-fpm.sock
srw-rw---- 1 www-data www-data 0 Dec 27 09:05 /run/php/php8.2-fpm.sock

What it means: Socket exists and is owned by www-data. Apache must run as www-data (or be in that group) to connect.

Decision: If you see “permission denied” in Apache error logs, fix socket ownership/group or Apache user, not WordPress.

Task 8: Read Apache errors around the failure time (don’t skim)

cr0x@server:~$ sudo tail -n 60 /var/log/apache2/error.log
[Wed Dec 27 09:11:02.331221 2025] [security2:error] [pid 22190] [client 198.51.100.24:51422] [id "949110"] [msg "Inbound Anomaly Score Exceeded"] [uri "/wp-admin/admin-ajax.php"]
[Wed Dec 27 09:11:02.331339 2025] [security2:error] [pid 22190] [client 198.51.100.24:51422] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5'"...

What it means: mod_security blocked a WordPress AJAX endpoint. The rule id is visible (949110 here).

Decision: Create a targeted exception (remove or modify that rule for this vhost/path), and verify you’re not masking a real attack. Use audit logs to validate patterns.

Task 9: Inspect ModSecurity audit log for full context

cr0x@server:~$ sudo grep -n "949110" /var/log/apache2/modsec_audit.log | tail -n 5
245817:Message: Warning. Pattern match "..." at ARGS:action. [file "/usr/share/modsecurity-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "78"] [id "949110"]

What it means: You have the CRS rule file and line number. That’s actionable.

Decision: Prefer exclusions by rule id + narrow location (e.g., only wp-admin/admin-ajax.php) rather than globally disabling the CRS.

Task 10: Prove rewrite behavior with RewriteLog-like tracing (Apache 2.4 method)

Apache 2.4 doesn’t use the old RewriteLog directive. You enable per-module trace logging:

cr0x@server:~$ sudo apachectl -t -D DUMP_VHOSTS | sed -n '1,40p'
VirtualHost configuration:
*:80                   example.com (/etc/apache2/sites-enabled/example.conf:1)
*:443                  example.com (/etc/apache2/sites-enabled/example-ssl.conf:1)

Temporarily add (for a single vhost) something like:
LogLevel warn rewrite:trace3
then reload and reproduce. Afterward, inspect logs:

cr0x@server:~$ sudo tail -n 40 /var/log/apache2/error.log
[rewrite:trace3] [pid 22501] mod_rewrite.c(477): [client 198.51.100.24:52011]  applying pattern '^/(.*)$' to uri '/'
[rewrite:trace3] [pid 22501] mod_rewrite.c(477): [client 198.51.100.24:52011]  rewrite '/' -> '/index.php'

What it means: You can see which patterns matched and how the URI was rewritten.

Decision: If a rule unexpectedly rewrites to a redirect, remove or constrain it. If WordPress rules never run, your directory context/AllowOverride is wrong.

Task 11: Detect “wrong scheme behind proxy” by checking request headers and Apache env

cr0x@server:~$ curl -s -D - -o /dev/null https://example.com/wp-login.php | egrep -i 'location:|server:|set-cookie:'
server: Apache
location: http://example.com/wp-login.php?redirect_to=...

What it means: WordPress is generating HTTP redirects while the browser uses HTTPS. Usually missing X-Forwarded-Proto handling or wrong WordPress site URL.

Decision: Fix proxy header propagation and tell Apache/WordPress the original scheme. Also check WordPress siteurl/home settings.

Task 12: Verify KeepAlive and timeouts (slow or “randomly failing” sites)

cr0x@server:~$ sudo apachectl -t -D DUMP_RUN_CFG | egrep -i 'KeepAlive|Timeout|MaxKeepAliveRequests|KeepAliveTimeout'
KeepAlive: On
MaxKeepAliveRequests: 100
KeepAliveTimeout: 5
Timeout: 60

What it means: Reasonable defaults. If KeepAliveTimeout is huge, you can exhaust workers under load.

Decision: For busy WordPress sites, keep KeepAliveTimeout low (usually 2–5 seconds) and let HTTP/2 multiplexing do the work.

Task 13: Check worker saturation in Apache server-status (if enabled)

cr0x@server:~$ curl -s http://127.0.0.1/server-status?auto | egrep 'BusyWorkers|IdleWorkers|ReqPerSec|CPULoad'
CPULoad: .322
ReqPerSec: 18.2
BusyWorkers: 245
IdleWorkers: 0

What it means: You are out of idle workers. New connections will queue or fail. This is an Apache capacity issue, not WordPress “being slow.”

Decision: Increase MaxRequestWorkers only if you have memory headroom. Otherwise reduce KeepAliveTimeout, fix slow backend calls, and scale out.

Task 14: Check PHP-FPM saturation (max children reached)

cr0x@server:~$ sudo tail -n 40 /var/log/php8.2-fpm.log
[27-Dec-2025 09:22:14] WARNING: [pool www] server reached pm.max_children setting (40), consider raising it
[27-Dec-2025 09:22:20] NOTICE: [pool www] child 17732 started

What it means: FPM is saturated. Apache may be fine; PHP workers aren’t.

Decision: Don’t blindly raise pm.max_children. Measure memory per PHP process, confirm DB latency, and tune based on RAM and workload.

Task 15: Confirm static assets are cached correctly (and not HTML)

cr0x@server:~$ curl -sI https://example.com/wp-content/themes/site/style.css | egrep -i 'cache-control|expires|etag|vary|content-encoding'
cache-control: public, max-age=31536000
etag: "2c1b-5f3c1a4b"
vary: Accept-Encoding
content-encoding: br

What it means: Great for versioned assets. If your theme uses unversioned filenames, this can freeze old CSS for users.

Decision: Use versioned asset URLs (query string or hashed filenames) if you set long max-age. Do not apply this caching to HTML responses.

Task 16: Quickly detect a “cache everything” mistake on HTML

cr0x@server:~$ curl -sI https://example.com/ | egrep -i 'cache-control|set-cookie|vary'
cache-control: public, max-age=31536000
set-cookie: wordpress_logged_in=...

What it means: You’re caching HTML publicly and setting auth cookies. That’s how you end up with one user’s session sprinkled into another user’s day.

Decision: Fix cache policy: HTML should usually be private or no-store for logged-in paths, and carefully controlled for anonymous traffic.

Three corporate-world mini-stories from the trenches

Mini-story 1: The incident caused by a wrong assumption

A mid-sized company migrated a WordPress marketing site behind a load balancer that terminated TLS.
The Apache origin only spoke HTTP internally, which is normal and fine—if you teach the app what reality looks like.
Someone said, “WordPress will figure it out.” That sentence should be banned from production chat.

Within minutes of the cutover, mobile users got stuck in a redirect loop. Desktop users sometimes got through.
The HTTP traces showed a pattern: requests arrived at the load balancer via HTTPS, were forwarded to Apache as HTTP,
and WordPress generated redirects back to HTTP because it believed it was running on plain HTTP.

The team initially chased rewrite rules. Then they chased HSTS. Then they chased the CDN.
The breakthrough came from doing the boring thing: capturing a full request/response chain from the origin with headers,
then checking what headers the load balancer actually set.

The origin didn’t receive X-Forwarded-Proto: https consistently because one listener was missing a header injection rule.
WordPress saw mixed signals and tried to “fix” URLs in both directions. The loop wasn’t mysterious; it was deterministic.

Fix: make the proxy headers consistent, configure Apache to trust the proxy, and configure WordPress to treat the forwarded proto as authoritative.
Then simplify Apache rewrites: one canonical redirect, one. The outage ended. The postmortem included a new rule:
no infrastructure migration without a curl transcript in the ticket.

Mini-story 2: The optimization that backfired

Another organization decided to “improve performance” by disabling .htaccess scanning.
The idea was right: AllowOverride None reduces filesystem lookups and can remove a class of config drift.
The execution was the problem: they flipped the switch on Friday afternoon with no rewrite rules moved into vhost config.

The site didn’t fully go down. It did something worse: it mostly worked, except for everything that made it a website.
The homepage loaded, but blog posts 404’d. Category pages 404’d. The JSON endpoints used by the block editor failed.
Support tickets described it as “random.” Nothing is random; it was path-based routing falling apart.

They rolled back and called it an “Apache issue.” Not quite. It was a change management issue.
Disabling AllowOverride is a migration. Treat it as a migration: replicate the rules, test permalinks, test wp-admin, test wp-json,
and only then remove .htaccess support.

The long-term outcome was positive: they eventually moved rules into the vhost, added integration tests that curl a set of representative URLs,
and removed a bunch of outdated plugin rewrite fragments from .htaccess. Performance improved, and the config got saner.
But the lesson was simple: optimizations don’t count if they delete functionality.

Mini-story 3: The boring but correct practice that saved the day

A large enterprise had a WordPress fleet with a common Apache baseline: standard modules, standard headers, standard TLS policy.
Each site also had its own plugins and editorial staff, which is a polite way of saying “unpredictable request payloads.”
They ran mod_security with the CRS, because compliance demanded it.

Every time a plugin update triggered false positives, other teams wanted to disable the WAF “temporarily.”
The platform team refused. Instead they had a procedure: collect the mod_security audit event, identify rule ID,
confirm the payload is legitimate, write a narrow exclusion for the site and endpoint, and attach it to a change request.
It was tedious. It was also repeatable.

One Monday morning a wave of requests started hitting /wp-login.php with weird parameter patterns.
mod_security blocked most of it. A handful of legitimate admin actions were also blocked, and editors complained quickly.
The team used the established workflow: they tuned one rule for a specific admin-ajax call while keeping the broader protections.

The site stayed up, authentication remained protected, and the only real “damage” was a 20-minute meeting to approve a rule exception.
Nobody wrote a heroic war story about it. That’s the point. Boring practice beats exciting outages.

Common mistakes: symptom → root cause → fix

Redirect loop (ERR_TOO_MANY_REDIRECTS)

Symptom: Browser loops between HTTP and HTTPS, or between www and non-www.

Root cause: Conflicting canonicalization across Apache rewrite, WordPress settings, and proxy/CDN behavior.

Fix: Pick one layer to enforce canonical host/scheme (usually edge or Apache). Ensure X-Forwarded-Proto and X-Forwarded-Host are consistent, and WordPress siteurl/home match the public URL.

Permalinks 404, but the homepage works

Symptom: / loads, /2025/… 404s, /wp-json fails.

Root cause: .htaccess ignored due to AllowOverride None, or rewrite rules missing in vhost config.

Fix: Enable AllowOverride FileInfo for the docroot directory or migrate WordPress rewrite rules into the vhost.

403 Forbidden on wp-admin or admin-ajax

Symptom: Admin pages load partially; AJAX calls fail; editor shows errors; logs show 403.

Root cause: mod_security false positive or Apache authz misconfiguration (Require rules, blocked methods, etc.).

Fix: Check Apache error log for rule IDs; tune mod_security exceptions narrowly. If it’s authz, fix directory permissions and Require all granted where appropriate.

502 Bad Gateway after “minor” PHP upgrade

Symptom: Apache returns 502 for all PHP pages; static files still serve.

Root cause: Apache proxy_fcgi still points to an old PHP-FPM socket path, or FPM service not running.

Fix: Validate SetHandler socket path and service state; reload Apache after updating. Confirm socket permissions.

Slow site, CPU looks fine, but users complain

Symptom: High latency, occasional timeouts, no obvious CPU spike.

Root cause: Worker starvation from long KeepAliveTimeout, backend saturation (FPM max children), or disk IO waits for PHP sessions/uploads.

Fix: Inspect BusyWorkers/IdleWorkers, FPM logs for max_children, and reduce KeepAliveTimeout. Then profile slow endpoints.

Theme/JS changes don’t show up for users

Symptom: “I cleared my cache” becomes the help desk’s cardio routine.

Root cause: Aggressive Cache-Control/Expires for unversioned assets.

Fix: Version assets (hash filenames or query string). Keep long cache for versioned assets only; shorter cache for dynamic or frequently updated files.

REST API errors, block editor broken, but front-end looks okay

Symptom: Editor can’t save, shows “Publishing failed,” or embeds fail.

Root cause: mod_security blocks JSON payloads, rewrite rules don’t route /wp-json, or headers/CORS misconfigured.

Fix: Confirm /wp-json/ returns 200 from origin, tune mod_security, and ensure rewrite/front controller rules apply to that path.

Joke #2: The most dangerous phrase in ops is “it’s just a header change.”

Checklists / step-by-step plan

Checklist A: Stabilize a broken WordPress site on Apache (30–60 minutes)

  1. Capture one failing request with curl (-svL) and save the output in the incident ticket.
  2. Correlate timestamp to Apache error log and PHP-FPM log.
  3. Classify as redirect/403/5xx/slow and pick the relevant module set to inspect.
  4. Confirm whether .htaccess is honored (AllowOverride) and whether rewrite rules exist where expected.
  5. Check mod_security hits by rule ID; don’t guess.
  6. Validate PHP-FPM socket path and permissions; confirm service is healthy.
  7. Measure worker saturation (Apache BusyWorkers/IdleWorkers; FPM max_children warnings).
  8. Make one change that removes the primary failure mode (e.g., remove conflicting redirect, add a narrow WAF exclusion, correct socket path).
  9. Rollback plan: ensure you can revert config quickly (previous vhost file, known-good module state).
  10. Re-test: homepage, one permalink, wp-admin login, admin-ajax, wp-json.

Checklist B: Prevent the next outage (the part nobody schedules)

  1. Standardize canonicalization: decide where redirects live (edge vs Apache vs app). Document it.
  2. Stop duplicating rules across Apache and WordPress. One canonical redirect chain should exist, not three.
  3. Move rewrite config into vhost if you can, and keep AllowOverride None for performance and determinism.
  4. Log what you need: request time, upstream time, status, user agent, host, X-Forwarded-Proto.
  5. Keep mod_security, but manage it: audit log retention, rule ID-based exceptions, and a simple approval process.
  6. Load test with realism: include wp-admin and wp-json calls, not just anonymous homepage hits.
  7. Limit blast radius: per-vhost configs, staged rollouts, and feature flags for risky modules (HTTP/2, brotli) where possible.

Checklist C: Safe Apache change process for WordPress

  1. Diff the config in a PR (or at least a change request) and require a second pair of eyes.
  2. Run apachectl -t before reload.
  3. Deploy to one canary vhost/host and run a scripted curl test suite (front page, permalinks, wp-login, wp-admin, wp-json).
  4. Watch error logs and 4xx/5xx rates for 15–30 minutes.
  5. Roll out gradually. If your tooling doesn’t support gradual rollouts, your tooling is the risk.

FAQ

1) Should I use .htaccess for WordPress on Apache?

If you’re on shared hosting, you probably have to. If you operate the server, prefer vhost config.
.htaccess costs performance and creates “invisible config” that your deployment process doesn’t version well.

2) What’s the single fastest way to tell if rewrite rules are the problem?

Curl a known permalink and watch the status code. Then check whether Apache honors .htaccess (AllowOverride).
If permalinks 404 and AllowOverride is None, you’re done: Apache is ignoring the rules.

3) Why does wp-admin work but the block editor fails?

The block editor leans heavily on REST API endpoints under /wp-json and AJAX calls.
mod_security, broken rewrites, or blocked request methods can selectively break those endpoints while leaving HTML pages intact.

4) Is mod_security worth the trouble for WordPress?

Yes, if you run it like an adult: audit logs, targeted exceptions, and periodic review.
No, if your only operational mode is “disable it when it yells.”

5) prefork vs event MPM: what should I pick for WordPress?

If you’re using PHP-FPM, use event MPM in most cases. If you’re using mod_php (not recommended for modern setups), you’ll be stuck with prefork.
The right answer is usually “event + PHP-FPM,” then tune workers based on memory and traffic.

6) Can HTTP/2 break my WordPress site?

Usually it won’t, but it can expose misconfigurations: TLS quirks, buggy proxies, or incorrect caching behavior.
If enabling HTTP/2 correlates with client hangs or odd partial loads, test by disabling HTTP/2 temporarily on one vhost and compare.

7) Why do I get 403 only on admin-ajax.php?

admin-ajax often carries payloads that look like attacks to generic WAF rules.
Confirm in Apache error logs; you’ll often see a mod_security rule ID. Exclude narrowly for that endpoint and rule ID.

8) My CSS/JS is cached forever after I enabled mod_expires. How do I fix it without turning caching off?

Keep long cache lifetimes for versioned assets only. If filenames aren’t versioned, add versions (hashes or query strings) in your theme build/deploy.
Shorten cache for unversioned assets. Caching is not the enemy; unversioned assets are.

9) WordPress keeps redirecting to HTTP even though my site is HTTPS. Is that Apache or WordPress?

It’s almost always “proxy reality mismatch.” Apache sees HTTP from the load balancer and passes that impression to PHP.
Fix forwarded proto/host handling and WordPress site URL settings, then remove duplicate redirects.

10) What’s a safe minimum set of Apache modules for WordPress?

Typically: rewrite, headers, TLS modules, a PHP handler (proxy_fcgi), and optionally deflate or brotli.
Add http2 if your environment supports it cleanly. Add mod_security if you can operate it properly.

Conclusion: what to do next (and what to stop doing)

WordPress gets blamed for a lot of Apache problems because it’s the thing users see.
In practice, the repeat offenders are configuration layers: rewrite rules fighting each other, .htaccess being ignored,
WAF rules blocking legitimate traffic, and “performance improvements” that weren’t tested against real endpoints.

Next steps that pay off immediately:

  • Write down one canonicalization policy (host + scheme) and enforce it in one place.
  • Decide whether you’re a .htaccess shop or a vhost-config shop. Pick one. Run it consistently.
  • Enable the logs you need to debug quickly (including request time), and make “curl transcript” part of incident hygiene.
  • Treat mod_security exceptions like code: narrow, reviewed, and justified.
  • When you tune Apache, tune it with capacity measurements (BusyWorkers/FPM saturation), not vibes.

Stop doing this:

  • Stop stacking redirects at three layers and then acting surprised when they form a Möbius strip.
  • Stop disabling security controls globally because one plugin update got spicy.
  • Stop shipping caching headers for HTML the same way you ship caching headers for images.

The goal isn’t a clever Apache config. The goal is a WordPress site that stays up, stays fast, and fails in ways you can diagnose before lunch.

← Previous
MySQL vs PostgreSQL: Docker memory limits—how to stop silent throttling
Next →
Real-world CPU testing: a simple method for your own workload

Leave a comment