Code

Debug a plugin conflict causing a hook to fire twice

A duplicate-order-confirmation-email scenario reasoning through revision saves, double-loaded plugin files, and recursive save_post calls before blaming a race condition.

Works with Claude / GPT1,500 uses 4.4

The prompt

code-debug-hook-firing-twice
Customers are getting the order confirmation email twice for every order. The email is
sent from a function hooked on `save_post_shop_order`:

```php
add_action('save_post_shop_order', 'myplugin_send_confirmation_email', 10, 3);
```

CONTEXT: [this started after a caching/performance plugin was installed, and adding a
`error_log('firing')` inside the function shows it logging twice per order, with the
second call happening about 200ms after the first]

Debug this like a senior dev chasing a duplicate-hook-fire bug, in order:
1. Rule out the most common cause first: `save_post` (and its post-type-specific
   variants like `save_post_shop_order`) fires on every save of that post type,
   including revision saves and autosaves, not just the "real" save. Check whether
   something is triggering a revision or an autosave shortly after the main save (a
   220ms gap is consistent with a second, near-immediate `wp_insert_post()` call
   rather than a true simultaneous race), and add `wp_is_post_revision()` /
   `wp_is_post_autosave()` guards if missing.
2. Check whether the plugin file itself is being loaded twice: search for the
   `add_action()` call across the codebase, and check whether it's registered inside a
   class constructor that could be instantiated more than once (a common cause is a
   plugin file being required both directly and via an autoloader, or a symlinked
   plugin folder appearing twice in the plugins directory listing).
3. Investigate whether the function itself calls `wp_update_post()` or
   `update_post_meta()` in a way that re-triggers `save_post` recursively (updating the
   post inside its own save hook is a classic cause of an apparent "double fire" that's
   actually a single external trigger plus one self-inflicted recursive one).
4. Now weigh the environmental clue seriously: since this started right after
   installing a caching/performance plugin, check whether that plugin does anything
   with object cache priming or "preload" requests that could trigger a duplicate
   save-adjacent action, or whether it hooks `save_post` itself for cache invalidation
   in a way that inadvertently calls back into order-processing logic.
5. Once the actual duplicate trigger is identified, fix it at the source (guard
   against revisions, remove the recursive update, or unhook the caching plugin's
   interference) rather than papering over it with a "has this email already been
   sent" meta flag, though note that flag is still worth adding as a defense-in-depth
   safety net regardless of the root cause, since duplicate sends to customers are
   costly enough to warrant a belt-and-suspenders fix.

Replace the hook, post type, and email logic with your actual scenario.

The 200ms timing detail matters more than it looks: a true race condition from concurrent requests looks different in logs (near-simultaneous, sub-10ms apart) than a sequential double-trigger from a revision save or a recursive update, which is why step 1 leads with timing analysis rather than jumping to “must be a caching conflict.”