Your Taxonomy’s Rewrite Rules Didn’t Flush: Inspecting rewrite_rules Instead of Guessing

Your Taxonomy’s Rewrite Rules Didn’t Flush: Inspecting rewrite_rules Instead of Guessing

The symptom is familiar. You register a custom taxonomy on init at priority 20, deploy, and the term archives return 404s. The taxonomy appears in the admin. Terms save. The REST API exposes it. But /genre/noir/ resolves to nothing, or worse, to a single post whose slug happens to match.

The reflex is to add flush_rewrite_rules() to the registration callback. That sometimes works, which is the problem: it teaches you the wrong lesson. The real question is not whether to flush, but whether the taxonomy’s rules were present in the registry at the moment the flush ran. The rewrite_rules option is the observable record of that moment. Read it before you touch anything.

Trace: what actually happens when you register a taxonomy

register_taxonomy() is documented with a blunt constraint: do not use it before the init hook. The rewrite argument is what triggers rule generation for the taxonomy. It defaults to true, using the taxonomy key as the slug, and can be set to false to prevent rewrite entirely. It also accepts an array with slug, with_front, hierarchical, and ep_mask keys.

Registration does not write to the database. It populates the global taxonomy registry and, through WP_Taxonomy::add_rewrite_rules(), adds a permastruct and rewrite tags to the $wp_rewrite object in memory. The rewrite_rules option is only written when something calls flush_rewrite_rules() or WP_Rewrite::flush_rules(). That distinction is the whole failure mode: registration is in-memory, flushing is persistent, and the two can happen in either order.

The init hook fires after WordPress has finished loading but before headers are sent, and many plugins instantiate themselves on it. Rewrite rules are usually called inside init, and the documentation for WP_Rewrite notes that permalinks must be refreshed before rewrite changes take effect. Priority 20 is not inherently wrong. It is simply late enough that any callback at the default priority 10 which flushes rules will run before your taxonomy exists in the registry. The flush then writes a rules array that has no entry for your taxonomy, and the option looks flushed while being wrong.

Root cause: the flush ran before the rules existed

flush_rewrite_rules() removes the rewrite rules and recreates them. Its $hard parameter controls whether .htaccess is updated or only the rewrite_rules option. The function documentation is explicit that this is an expensive operation and should only be used when necessary. It regenerates from whatever is registered at the moment of the call. If your taxonomy is registered at priority 20 and a plugin flushes at priority 10, the regenerated array is missing your taxonomy’s permastruct. Nothing errors. The option is simply incomplete.

This is why adding a flush inside your own registration callback often appears to fix the problem. Your callback runs at priority 20, after registration, so the flush sees the taxonomy. But it also runs on every request, which turns a one-time correctness problem into a permanent performance problem. The documentation’s warning about cost is not decorative.

There is a second, less common cause worth ruling out before you change priorities: the rewrite argument itself. If rewrite is false, or the slug collides with an existing permastruct, or with_front produces a path that conflicts with the permalink structure, the rules may be absent from the option after a correctly timed flush. Inspecting the option distinguishes these cases.

Inspect the rewrite_rules option directly

The option stores an array of regex patterns mapped to query strings. get_option() unserializes stored array values and returns them as arrays, so you can read it without decoding. It is autoloaded, so reading it does not add a query on a normal request.

From WP-CLI, the fastest check is:

wp option get rewrite_rules --format=json | jq 'keys[]' | grep -i 'genre'

Replace genre with your taxonomy slug. If nothing matches, the taxonomy’s rules are not in the stored option. If matches appear, the rules exist and the problem is elsewhere: a conflicting rule earlier in the array, a missing rewrite tag, or a query var that is not registered.

To see the full structure without JSON tooling:

wp eval '
$rules = get_option( "rewrite_rules", array() );
foreach ( $rules as $regex => $query ) {
    if ( false !== strpos( $regex, "genre" ) || false !== strpos( $query, "genre" ) ) {
        echo $regex . " => " . $query . PHP_EOL;
    }
}
'

For a direct database read, bypassing the options cache:

SELECT option_value FROM wp_options WHERE option_name = 'rewrite_rules';

The value is a serialized PHP array. It is useful for confirming the option exists and is not empty, but the WP-CLI or get_option() route is easier to compare against expected patterns.

What should you expect to see? For a taxonomy registered with slug genre and default with_front, the rules include a pattern resembling genre/([^/]+)/?$ mapping to index.php?genre=$matches[1], plus feed and pagination variants. The exact regex depends on the permalink structure and the hierarchical argument. Compare the stored keys against the permastruct the taxonomy should have produced, not against a remembered string.

Diagnostic sequence

Work through these in order. Each step narrows the cause rather than re-flushing and hoping.

  1. Confirm the taxonomy is registered on the current request. Run wp eval 'var_dump( taxonomy_exists( "genre" ) );'. If false, the registration callback is not running where you think it is, and rewrite rules are a distraction.
  2. Dump the stored rules and search for the taxonomy slug. Use the WP-CLI commands above. Absence means the last flush did not see the taxonomy. Presence means the rules were written and the failure is downstream.
  3. Check the flush timing. If the rules are absent, find every flush_rewrite_rules() call in plugins and theme code and note its hook and priority. A flush at init priority 10 will not see a taxonomy registered at priority 20. This is the most common cause and the one the priority-20 detail points toward, but confirm it rather than assuming it.
  4. Check the rewrite argument. If the rules are absent even after a flush that demonstrably ran after registration, inspect the registration arguments. rewrite => false, a slug collision, or a with_front setting that places the taxonomy under a path already claimed by another rule will produce missing or shadowed rules.
  5. Check for rule conflicts. If the rules are present but requests still 404, look at rule order. Rules are matched in order, and a broader pattern earlier in the array can consume the request before your taxonomy rule is reached. The add_rewrite_rule() documentation notes that rules are applied in the order added, regardless of top or bottom placement, and that a broad rule can prevent a later, more specific one from ever matching.

Fix: flush once, after registration, gated on a stored version

The correct pattern is a one-time flush that runs after the taxonomy is registered and only when the rules need regenerating. The documentation’s user-contributed notes show the shape: read the rewrite_rules option, check whether the expected regex is present, and flush only if it is not. A stored version option is more robust because it also handles slug changes.

add_action( 'init', 'myplugin_register_genre_taxonomy', 20 );
function myplugin_register_genre_taxonomy() {
    register_taxonomy( 'genre', array( 'post' ), array(
        'public'       => true,
        'show_in_rest' => true,
        'rewrite'      => array( 'slug' => 'genre', 'with_front' => false ),
    ) );
}

add_action( 'init', 'myplugin_maybe_flush_genre_rules', 99 );
function myplugin_maybe_flush_genre_rules() {
    $version = '2026-10-genre-v1';
    if ( get_option( 'myplugin_genre_rules_version' ) === $version ) {
        return;
    }
    flush_rewrite_rules( false );
    update_option( 'myplugin_genre_rules_version', $version );
}

Two details matter. The flush runs at priority 99, after the taxonomy registration at 20, so the registry is populated. The false argument performs a soft flush, updating only the rewrite_rules option rather than writing .htaccess; use a hard flush only when the server configuration genuinely needs rewriting. The version string is bumped whenever the slug or rewrite arguments change, which is the deployable artifact the option represents.

If you cannot control the other plugin’s flush at priority 10, moving your registration earlier is the alternative. Registering at the default priority 10 or lower places the taxonomy in the registry before most flushes. Priority 20 is a legitimate choice when you need to run after another registration, but it carries this ordering cost and should be paired with a gated flush at a later priority.

Verification

After deploying the fix, verify the stored option rather than the browser. Run the WP-CLI search for the taxonomy slug and confirm the expected regex keys are present. Then request a term archive and check the query vars in Query Monitor’s request panel: the genre query var should be set and the main query should resolve to a taxonomy archive, not a 404 or a single post.

If the rules are present and the query var is set but the template is wrong, the problem has moved to template hierarchy or template_include, not rewrite. If the rules are present and the query var is absent, check that the taxonomy is publicly queryable and that its query_var argument has not been disabled. The register_taxonomy() documentation warns that setting query_var to true rather than a string can produce 404s; use the taxonomy key or a custom string.

One more check belongs in the verification step: confirm the taxonomy is exposed in the block editor if editors rely on it. show_in_rest => true is required for the taxonomy to appear in the block editor’s term panels. A taxonomy that saves terms in the classic editor but not in the block editor is a REST registration problem, not a rewrite problem, and the rewrite_rules option will look perfectly healthy while it happens.

Why not flush on every init

Flushing on every request is the failure mode that hides the original bug. It works, in the sense that the rules are always current, but it regenerates the entire rules array on every page load. The documentation calls the operation expensive and says it should only be used when necessary. On a small publishing site the cost may be tolerable; on a site with a large permalink structure and several plugins adding rules, it is a measurable tax paid to avoid diagnosing an ordering problem once.

The gated flush is not more complex. It is a version option and a conditional. The difference is that it fails visibly when the version is not bumped, which is a deploy-time mistake you can catch, rather than silently masking a registration-order bug indefinitely.

FAQ

Does priority 20 on init itself prevent rewrite rules from flushing?

No. Priority 20 does not block anything by itself. It places your registration after callbacks at priority 10, so a flush triggered at priority 10 runs before your taxonomy exists in the registry. The failure is ordering, not the priority number in isolation. Confirm by checking whether the taxonomy’s rules are absent from the stored option and whether another component flushes earlier.

Why do the rules appear after I click Save on the Permalinks screen?

Saving the Permalinks screen triggers a flush in the admin context, after all plugins have loaded and registered their taxonomies. That flush sees your taxonomy and writes the correct rules. It is a manual version of the gated flush described above. The fact that it works is evidence that registration is fine and the automatic flush timing is the problem.

Can I just delete the rewrite_rules option instead of flushing?

Deleting the option forces WordPress to regenerate the rules on the next request, after plugins have loaded. A user-contributed note on the flush_rewrite_rules() documentation page recommends this as a workaround for activation hooks where the taxonomy is not yet registered. It is effective, but it is a workaround for the same ordering problem. The gated flush with a version option is more explicit about when regeneration happens and why.

What if the rules are present but the archive still 404s?

Check rule order and query var registration. A broader rule earlier in the array can consume the request. A taxonomy with query_var disabled or set to true can fail to resolve. Inspect the matched rule in Query Monitor and confirm the genre query var is set before the main query runs.

For a broader triage of front-end resolution failures on a new install, the sequence in What to Fix First When a New WordPress Site Says Nothing Found covers the permalink and query layers that sit beneath taxonomy-specific rules.

Sources

  • WordPress Developer Reference, register_taxonomy(): the init constraint and the rewrite argument.
  • WordPress Developer Reference, flush_rewrite_rules(): hard versus soft flush and the cost warning.
  • WordPress Developer Reference, init hook: when it fires and how plugins use it.
  • WordPress Developer Reference, WP_Rewrite: rules called inside init and the need to refresh permalinks.
  • WordPress Developer Reference, get_option(): array unserialization behavior.
  • WordPress Developer Reference, add_rewrite_rule(): rule ordering and the user-contributed note on checking rewrite_rules before flushing.