# You Restored It From the Trash and It's Still Gone: A WordPress Trash and Permanent Delete Guide

A site we took over last year had a page that 404'd for about three weeks before anyone said anything. It wasn't hacked, it wasn't a redirect problem, and the page was sitting right there in the admin where it always had been. Someone had trashed it by accident, noticed within the hour, and clicked Restore. WordPress put it back — as a draft. The row returned, the title returned, the URL kept pointing at nothing.

That behavior isn't a bug, and it isn't new. It's been WordPress's documented default since version 5.6. But almost nobody knows about it, because the Trash looks like the one part of WordPress that works exactly the way you'd guess: things go in, things come out, nothing is lost. Under that friendly recycle-bin metaphor there's a real state machine that renames your slugs, reassigns your post statuses, quietly holds onto comments, and runs a deletion timer that most site owners have never heard of.

This guide walks through what actually happens to a post between "Move to Trash" and gone forever — the database rows, the slug rename, the thirty-day clock, and the one category of content WordPress deletes permanently the moment you click, with no trash and no undo.

---

## What the Trash Actually Does to a Post

The Trash arrived in WordPress 2.9, and it's less of a folder than a status. When you trash a post, nothing moves and nothing is copied. WordPress runs `wp_trash_post()`, which flips the post's `post_status` column to `trash` and leaves the row exactly where it was in `wp_posts`.

### The row stays put for as long as it's trashed

Every trashed post keeps its ID, its full content, its meta, and its revisions. The Trash view in wp-admin is just a query filtered to that one status. This matters more than it sounds: a site with two thousand trashed posts is carrying two thousand full posts' worth of content, postmeta, and revision history in the tables that every front-end query has to work around.

Before the status flip, core writes two pieces of status metadata to the post so it can find its way home later:

- `_wp_trash_meta_status` — the status the post had immediately before it was trashed
- `_wp_trash_meta_time` — a Unix timestamp of the moment it went in, which is what the deletion timer reads

Comments get pulled in too. Core caches each comment's approval status in a meta key of its own and then rewrites every comment on that post to the status `post-trashed`, so they vanish from your moderation queue without being deleted. Restore the post and core puts them back to the statuses it cached, with one guard: a comment whose cached status was itself `post-trashed` comes back as unapproved.

### Your slug gets renamed out from under you

**This is the part that surprises developers.** A trashed post can't be allowed to squat on its URL, because you might want to republish something at that address. So core appends a suffix, and your slug `annual-report` becomes `annual-report__trashed` in the database.

Before it does that, it stores the original in a meta key called `_wp_desired_post_slug`. Core's own comment on that function says the slug is saved so the post can "try to reclaim it" on the way back out — and the hedge in that sentence is doing real work, as we'll get to. The rename also truncates: the slug is cut to 191 characters before the nine-character suffix is added, which keeps the result inside the 200-character column.

**WHY IT MATTERS**

Trashing is reversible, but it is not *neutral*. A round trip through the Trash can change a post's status and change its URL — two edits you didn't ask for and won't see reflected anywhere on the edit screen — and it hides the post's comments for as long as it sits there. On a live site, the status change alone is enough to take a working page offline while the post still looks perfectly normal in the admin.

## The Restore That Doesn't Restore

Click Restore and WordPress calls `wp_untrash_post()`. It reads back the previous status it saved, fires its hooks, deletes the two trash meta keys, and updates the post. The thing it does not do is put the post back the way it was.

### Since 5.6, restored posts come back as drafts

Core sets the new status to `draft` for everything except attachments, which return to `inherit`. Your previously published post is now an unpublished draft. WordPress's own function reference documents the change plainly: as of 5.6, "an untrashed post is now returned to 'draft' status by default, except for attachments which are returned to their original 'inherit' status." Before 5.6, restored posts were always given their original status back.

There was a sensible reason for the change. Restoring straight to `publish` means one misclick in the Trash view instantly pushes content live, sometimes content that was trashed precisely because it shouldn't be public. Defaulting to draft makes restoration a two-step, deliberate act. It also means that on any site where an editor restores something and assumes they're done, the page stays dark until someone hits Publish.

If you'd rather have the old behavior, core ships the filter and even ships the callback for it. The `wp_untrash_post_status` filter receives the new status, the post ID, and the previous status, and core includes a ready-made function that just returns the third argument:

```
// Restore posts to the status they had before being trashed.
add_filter(
    'wp_untrash_post_status',
    'wp_untrash_post_set_previous_status',
    10,
    3
);
```

Put that in a small site-specific plugin rather than your theme's `functions.php`, so that switching themes later doesn't silently take the behavior with it.

### And the slug is only probably yours again

On the way out of the Trash, core looks for that `_wp_desired_post_slug` value, deletes the meta, and hands the old slug back to the post. Then it runs the result through `wp_unique_post_slug()` like any other save — which is where the reclaim can fail. If something else grabbed `annual-report` while your post was in the Trash, your restored post gets `annual-report-2` instead, and the original URL now belongs to a different piece of content.

Core does try to keep that from happening. When you publish or update a post whose slug matches a trashed post's slug, core goes and finds those trashed posts and suffixes them out of the way first. The `add_trashed_suffix_to_trashed_posts` filter, which defaults to true, is there if you ever need to switch that off. Core resolves the collision in favor of the live post, which is the right call — but it's also the moment your trashed post loses the clean version of that address. It will still try to reclaim the slug on restore; it just won't win the race.

## The Thirty-Day Timer You Never Set

Trashed content doesn't sit there forever. WordPress has been quietly deleting it on a schedule the entire time.

### EMPTY\_TRASH\_DAYS, and what it really controls

**The default retention window is 30 days.** It's set by a constant called `EMPTY_TRASH_DAYS`, which WordPress defines for you if your `wp-config.php` doesn't. The official wp-config documentation describes it as "the number of days before WordPress permanently deletes posts, pages, attachments, and comments, from the trash bin."

You can change it to any number of days, and setting it to zero disables the Trash altogether — every delete becomes immediate and permanent. The docs attach a warning to that one worth repeating: with the Trash disabled, WordPress "will not ask for confirmation when someone clicks on 'Delete Permanently' using this setting." Zero is a legitimate choice for a tightly controlled site, but it removes your last line of defense against a slip of the mouse.

```
// In wp-config.php, above the "That's all, stop editing!" line.

define( 'EMPTY_TRASH_DAYS', 30 );  // Default: 30 days
define( 'EMPTY_TRASH_DAYS', 7 );   // Tighter window
define( 'EMPTY_TRASH_DAYS', 0 );   // Disable the Trash entirely
```

### The cleanup runs on WP-Cron, which means "daily" is aspirational

The actual deletion is handled by a hook named `wp_scheduled_delete`, registered as a daily WP-Cron event. When it fires, it queries the postmeta table for every `_wp_trash_meta_time` older than your cutoff, confirms each post is still in the Trash, and calls `wp_delete_post()` on it. That deletes for good rather than re-trashing, because a post already sitting in the Trash skips the trash-instead-of-delete guard. It does the same pass over trashed comments. Anything whose status changed without going through a proper restore gets its stale trash meta cleaned up instead of being deleted. A normal restore is safer still: `wp_untrash_post()` deletes those trash meta keys itself, so the cleanup job stops matching the post at all.

Two details about that schedule are worth knowing. The event is registered from `wp-admin/admin.php`, so it gets scheduled the first time somebody loads an admin page. And WP-Cron isn't a real cron daemon — it only runs when someone visits the site. On a quiet site the cleanup is delayed until the next page load, not skipped, so trashed content can outlive its window by a while. If you want it punctual, disable WP-Cron in `wp-config.php` and hit `wp-cron.php` from a real system cron job.

You can see the whole schedule, including this one, from the command line:

```
wp cron event list --fields=hook,next_run_relative,recurrence
```

While you're in there, you'll spot a sibling event called `wp_scheduled_auto_draft_delete`. That one is unrelated to the Trash: it force-deletes abandoned auto-drafts — the empty placeholder posts WordPress creates the instant you open the editor — once they're more than seven days old.

**QUICK REFERENCE**

The two constants that govern all of this, and their core defaults:

`EMPTY_TRASH_DAYS` — default **30**. Days before trashed posts, pages and comments are permanently deleted. Set to 0 to switch the Trash off entirely.
`MEDIA_TRASH` — default **false**. When false, deleting an attachment is immediate and permanent. Set to **true** to give media a Trash of its own.

## Attachments Play by Different Rules

Here's the asymmetry that catches people out. Posts and pages get a thirty-day safety net by default. Images, PDFs and every other file in your Media Library get none.

### MEDIA\_TRASH is off unless you turn it on

Core defines `MEDIA_TRASH` as `false`. With it off, deleting an attachment skips the Trash entirely: the row goes, the postmeta goes, and the file is unlinked from disk. There is no restore, and no scheduled job to intercept — your only recovery path is a backup.

Turning it on is one line in `wp-config.php`, and it makes the Media Library behave like the posts list, with a Trash view and the same retention window:

```
define( 'MEDIA_TRASH', true );
```

Note that attachments need *both* constants to be truthy before they'll go to the Trash. If you've set `EMPTY_TRASH_DAYS` to zero to disable the Trash, enabling `MEDIA_TRASH` on its own won't give you a media Trash.

## What to Actually Do About It

Three decisions. The first two stand on their own; the third only takes effect if you've left `EMPTY_TRASH_DAYS` above zero.

### 1. Pick a retention window that matches how your team works

Thirty days is generous for a site where one person publishes and nobody deletes much. It's not generous at all for a site with several editors, where a mistake might not surface until the next quarterly review. If your content is mostly evergreen and your team is small, a shorter window like seven days keeps the tables tidy. If people are actively cleaning house, leave it at 30 or raise it, and lean on your backups for anything older.

### 2. Empty the Trash on purpose instead of waiting

**Deleting a post is a real database operation, not a flag flip.** Force-deleting removes the post row, its postmeta, its revisions, its comments and their meta, and its term relationships. Doing thousands of those in a single admin request is how people time out the Empty Trash button. WP-CLI handles it without the browser in the middle:

```
# See what you're carrying, by post type.
wp post list --post_status=trash --post_type=any \
  --fields=ID,post_type,post_title --format=count

# Look at the oldest ones before you commit to anything.
wp post list --post_status=trash --post_type=any \
  --fields=ID,post_type,post_title,post_modified --format=table

# Permanently delete every trashed post. Take a backup first.
wp post delete $(wp post list --post_status=trash \
  --post_type=any --format=ids) --force
```

Run the count first. On sites we've inherited, the number that comes back is routinely in the thousands, and it's almost always a mix of genuine cleanup and one afternoon where somebody bulk-trashed an old import. Read the list before you delete it, because the `--force` flag means exactly what it says.

### 3. Decide whether your Media Library deserves a net

If the people managing your media are the same people who write the content, `MEDIA_TRASH` is cheap insurance and the reason is obvious the first time somebody clears out "old" images that turned out to be in use. If your library is machine-generated — product imports, form uploads, anything that churns — a permanent delete may be exactly what you want, and the Trash just becomes another pile to manage.

### None of this replaces a backup

The Trash protects you from a misclick. It does nothing about a bad plugin update, a failed migration, or a hack, and its thirty-day window is short enough that the mistake you find in March is already unrecoverable from February. Treat it as an undo button with a timer, and keep real backups underneath it.

**THE FIVE-MINUTE VERSION**

Count what's in your Trash. If a post came back from it recently, check that it's actually published and that its URL still resolves. Set `EMPTY_TRASH_DAYS` deliberately instead of inheriting 30. And if losing a Media Library file would ruin your week, add `MEDIA_TRASH` before you need it, not after — and keep `EMPTY_TRASH_DAYS` above zero, or it won't do a thing.

---

## Frequently Asked Questions

Where do trashed WordPress posts actually go?Nowhere. The post row stays exactly where it was in the wp\_posts database table, and WordPress simply changes its post\_status column to trash. The Trash screen in wp-admin is a filtered query against that status, not a separate folder or table. The post keeps its ID, content, metadata and revisions the entire time it sits there.

Why did my restored post come back as a draft instead of published?That is the documented default behaviour as of WordPress 5.6. Restoring a post assigns it draft status rather than the status it held before it was trashed, with attachments being the exception since they return to inherit status. The change makes restoring a deliberate two-step action so that a stray click in the Trash view cannot push content straight back onto the live site. You can restore the old behaviour with the wp\_untrash\_post\_status filter and the callback core provides for it.

How long does WordPress keep posts in the Trash?Thirty days by default, controlled by the EMPTY\_TRASH\_DAYS constant, which you can redefine in wp-config.php. A daily WP-Cron event named wp\_scheduled\_delete does the actual removal by looking for trashed items whose recorded trash timestamp is older than that window. Because WP-Cron only fires when someone loads the site, a low-traffic site may hold onto trashed content past the thirty day mark until the next visit triggers the job.

Can I turn the WordPress Trash off completely?Yes. Setting EMPTY\_TRASH\_DAYS to zero in wp-config.php disables the Trash, and every deletion becomes immediate and permanent. WordPress documentation attaches a specific warning to this setting: with the Trash disabled, the software will not ask for confirmation when someone clicks Delete Permanently. It is a reasonable choice on a tightly controlled site, but it removes the last safeguard against an accidental click.

Why are deleted images gone forever when deleted posts are recoverable?Because media has its own constant and it is switched off by default. Core defines MEDIA\_TRASH as false, which means deleting an attachment removes the database rows and unlinks the file from disk immediately, with no Trash stage and no scheduled job to intercept it. Setting MEDIA\_TRASH to true in wp-config.php gives the Media Library a Trash view that follows the same retention window as posts. Attachments need both that constant and a non-zero EMPTY\_TRASH\_DAYS before they will go to the Trash.

Does emptying the Trash make my site faster?On most sites the effect is modest rather than dramatic. Front-end queries filter by post status, so trashed posts are not being served to visitors, and a few dozen of them will not be measurable. Where it does help is at scale: thousands of trashed posts carry their own postmeta, revisions, comments and term relationships, which enlarges the tables that every query and every backup has to work through. Clearing them is good database hygiene, and it is best treated as part of a broader cleanup rather than as a speed fix on its own.

If a page on your site went quiet and nobody can explain why, the Trash is worth checking before anything else. We audit and clean up WordPress installations for a living — [talk to amplifi.studio](https://amplifi.studio).

Built by [amplifi.studio](https://amplifi.studio) — see also [WordPress Kept Every Draft You Ever Saved: A WordPress Post Revisions Guide](https://amplifi.studio/wordpress-post-revisions-guide/).