WDT 404 Cache
Cacht die gerenderte 404-Seite für Gäste kurz als Datei und liefert sie aus, bevor WooCommerce, Theme und Hauptabfrage laden. Gegen Bots, die immer wieder dieselben nicht existierenden URLs abfragen.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/webdevtrust/wdt-404-cache/archive/refs/heads/main.zipCacht die gerenderte WordPress-404-Seite für Gäste kurz als Datei und liefert sie aus, bevor WooCommerce, das Theme und die Hauptabfrage laden.
Anlass: Bots fragen rund um die Uhr immer wieder dieselben, noch nicht veröffentlichten Produkt-URLs ab. Page-Caches und CDNs (z. B. Servebolt ACD) cachen 404-Antworten in der Regel nicht. Deshalb rendert PHP jeden Aufruf komplett (0,5–0,7 s). Das Plugin wirkt unabhängig von IP, CDN und Hoster.
Status: auf Staging getestet (WooCommerce-Shop auf Servebolt, Oktober 2026), siehe Staging-Ergebnisse.
Funktionsweise
- Speichern: Ist ein Aufruf eine 404 und cachebar (siehe unten), wird das
fertige HTML samt Response-Headern (ohne
Set-Cookie) komprimiert unterwp-content/cache/wdt-404-cache/<sha1>.cacheabgelegt. Gespeichert wird nur, wenn die Antwort wirklich Status 404 hat,text/htmlist, mit</html>endet, keinDONOTCACHEPAGEgesetzt ist und kein Login- oder Warenkorb-Cookie gesetzt wird. - Ausliefern: Beim nächsten Aufruf derselben URL liefert das Plugin den
Eintrag schon beim Laden seiner Hauptdatei aus: Status 404, gespeicherte
Header plus
X-WDT-404-Cache: HIT; age=<Sekunden>. Danach ist Schluss. WooCommerce, Germanized, Theme, Übersetzungen und Hauptabfrage laufen dann nicht. Plugins werden alphabetisch geladen, Plugins mit Namen vorwdt-…(z. B. Mollie, Rank Math, Servebolt) also noch eingebunden. Mit dem optionalen MU-Loader entfällt auch das.
Wann gecacht wird
GEToderHEAD, normaler Theme-Aufruf (index.php), kein Admin, AJAX, REST, Cron, WP-CLI oder XML-RPC- kein Cookie, dessen Name beginnt mit
wordpress_logged_in_,wordpress_sec_,wp-postpass_,comment_author_,woocommerce_items_in_cart,woocommerce_cart_hash,wp_woocommerce_session_oderwoocommerce_recently_viewed - keine Query-Parameter, außer ignorierten Tracking- und Cache-Buster-Parametern:
_,utm_*,gclid,gbraid,wbraid,fbclid,msclkid,mc_cid,mc_eid,_ga,_gl./produkt/x/?_=123nutzt also denselben Eintrag wie/produkt/x/. Jeder andere Parameter (z. B.?p=5) schaltet den Cache für diesen Aufruf ab.
Schlüssel: Schema + Host + Pfad.
Wann der Cache geleert wird
Komplett und sofort (und zur Sicherheit noch einmal am Ende des Requests):
- ein öffentlich sichtbarer Beitrag, eine Seite oder ein Produkt wird veröffentlicht, aktualisiert oder zurückgezogen (auch geplante Veröffentlichungen und Slug-Änderungen)
- Begriffe (Kategorien, Tags, Attribute) werden angelegt oder bearbeitet
- Rewrite-Regeln werden neu erzeugt (z. B. Permalinks speichern)
- Menüs oder Customizer werden gespeichert, Theme-Wechsel, Plugins (de)aktivieren oder aktualisieren
- Link „Cache leeren“ auf der Plugins-Seite
do_action('wdt_404_cache_flush');aus eigenem Code
Alles andere deckt die TTL ab, z. B. eine neue Weiterleitung in Rank Math oder Redirection. Dort gilt also bis zu TTL Sekunden noch die alte 404. Bei Bedarf den Cache von Hand leeren.
Schutz vor Überlauf
Höchstens WDT_404_CACHE_MAX_ENTRIES Dateien (Standard 500). Ist das Limit
erreicht, werden zuerst abgelaufene Einträge gelöscht. Ist danach noch kein
Platz, wird nichts Neues gespeichert. Zufällige Angriffspfade können das
Verzeichnis also nicht volllaufen lassen. Bodies werden mit zlib komprimiert.
Konfiguration (wp-config.php, alles optional)
define('WDT_404_CACHE_TTL', 300); // Sekunden, 1–3600, Standard 300
define('WDT_404_CACHE_MAX_ENTRIES', 500); // Standard 500
define('WDT_404_CACHE_DIR', '/pfad/'); // Standard wp-content/cache/wdt-404-cache/
define('WDT_404_CACHE_DISABLED', true); // Notschalter: nichts ausliefern, nichts speichern
TTL: Weil der Cache beim Veröffentlichen sofort geleert wird, ist eine längere TTL unkritisch. Bei einer URL, die alle 10 s abgefragt wird, rendert PHP pro TTL-Fenster einmal: Bei 60 s werden etwa 83 % der Aufrufe abgefangen, bei 300 s etwa 97 %.
Filter
Die Filter werden beim Ausliefern gelesen, also vor den normalen Plugins. Deshalb müssen sie aus einem MU-Plugin kommen (bzw. aus dem MU-Loader oder einem alphabetisch früheren MU-Plugin).
// weitere Cookies, die das Caching verhindern (Präfix-Vergleich)
add_filter('wdt_404_cache_bypass_cookies', fn (array $p) => [...$p, 'wcml_client_currency']);
// weitere ignorierbare Query-Parameter ("prefix_*" = Präfix)
add_filter('wdt_404_cache_ignored_query_args', fn (array $a) => [...$a, 'ref']);
Optional: MU-Loader
examples/mu-plugins/wdt-404-cache-loader.php
nach wp-content/mu-plugins/ kopieren. Dann wird ausgeliefert, bevor überhaupt
ein normales Plugin geladen ist. Der Loader wirkt nur, solange das Plugin
aktiv ist. Deaktivieren auf der Plugins-Seite schaltet also beides ab.
Status auf der Plugins-Seite
Unter der Beschreibung steht eine Statuszeile, z. B.:
✅ aktiv · 3 gültige Einträge (max. 500) · TTL 300 s · Auslieferung früh (MU-Loader) · zuletzt geleert 12.10.2025 14:03
Daneben gibt es den Link „Cache leeren“ (nur mit manage_options).
Einschränkungen
- Ausgeliefert wird das HTML so, wie es das Theme gerendert hat. Plugins, die
ihren eigenen Output-Buffer vor
template_redirectstarten (manche Minifier/Optimierer), sind im gecachten HTML nicht enthalten. Für eine 404-Seite ist das normalerweise nur ein kosmetischer Unterschied. - Inhalte, die sich nur nach User-Agent, Geolocation oder anderen hier nicht
erfassten Cookies unterscheiden, werden nicht getrennt gecacht. Bei Bedarf
das Cookie über
wdt_404_cache_bypass_cookiesergänzen. - Bei mehreren Webservern ohne gemeinsames Dateisystem hat jeder Server seinen eigenen Cache. Das funktioniert, beim Leeren über das Backend wird aber nur der Cache des Servers geleert, der den Admin-Request bearbeitet. Die TTL begrenzt das.
Tests
- Als Gast (ohne Cookies) eine nicht existierende URL zweimal abrufen:
curl -sI https://shop.example/produkt/gibt-es-nicht/ | grep -iE '^(HTTP|x-wdt)' curl -sI https://shop.example/produkt/gibt-es-nicht/ | grep -iE '^(HTTP|x-wdt)'Der erste Aufruf liefert
404ohne HIT-Header, der zweite404mitX-WDT-404-Cache: HIT. Der HTML-Body ist identisch (curl -s … | md5sum). …/?_=123liefert ebenfalls HIT,…/?foo=1nicht.- Mit Warenkorb-Cookie (
-H 'Cookie: woocommerce_items_in_cart=1') oder eingeloggt gibt es keinen HIT. - Einen Entwurf mit passendem Slug veröffentlichen: Die URL liefert sofort
200. - Antwortzeit vorher/nachher:
curl -so /dev/null -w '%{time_starttransfer}\n' …
Staging-Ergebnisse
Gemessen per curl von außen (TTFB = Antwortzeit bis zum ersten Byte) und
mit dem Mess-MU-Plugin aus wdt-third-party-performance (PHP-Zeit, Queries):
| Anfrage | TTFB | PHP-Zeit | Queries |
|---|---|---|---|
404 ohne Cache (z. B. ?foo=1, Warenkorb-Cookie) |
~0,7–0,8 s | ~630–700 ms | ~325–330 |
| 404-HIT ohne MU-Loader | ~0,13–0,15 s | ~50–70 ms | 23–28 |
| 404-HIT mit MU-Loader | ~0,09 s | geschätzt 5–10 ms¹ | – |
| statische Datei (nur Netzwerk, Vergleichswert) | ~0,08 s | – | – |
¹ Mit MU-Loader beendet das Plugin den Aufruf, bevor das Mess-Plugin lädt. Geschätzt aus dem Abstand zur statischen Datei.
Geprüft:
- Das ausgelieferte HTML ist identisch mit dem gerenderten; Status 404 und
Cache-Controlbleiben erhalten. ?_=…und?utm_*nutzen den Eintrag, andere Parameter und Warenkorb-Cookies nicht. HEAD funktioniert.- Nach dem Veröffentlichen eines Entwurfs mit dem gecachten Slug liefert die
URL sofort
200.
Dateien
wdt-404-cache.php Bootstrap: frühe Auslieferung, dann Speichern/Leeren registrieren
includes/NotFoundCache.php Cache: Ausliefern, Speichern, Invalidierung
includes/Admin.php Statuszeile, „Cache leeren“ auf der Plugins-Seite
uninstall.php Löscht das Cache-Verzeichnis (keine DB-Daten)
examples/mu-plugins/wdt-404-cache-loader.php Optionaler MU-Loader
Anforderungen
PHP 8.2+, WordPress. WooCommerce ist nicht nötig. Die Cookie-Liste berücksichtigt WooCommerce aber.
Lizenz
GPL-2.0-or-later, siehe LICENSE.