Bulk deletion is one of those jobs that feels harmless until a filter is one click wider than intended. WordPress already provides two strong tools: the dashboard for visible, recoverable batches and WP-CLI for precise automation. The safest workflow uses both ideas—preview first, trash second, verify third, purge last.

Choose the least powerful method that fits

  • Dashboard: best for tens or hundreds of posts that a human can filter and review. It uses WordPress permissions, hooks, and Trash behavior.

  • WP-CLI: best for repeatable selection, large inventories, remote operations, and auditable ID lists. Run it as the site owner from the correct WordPress installation.

  • A maintained deletion plugin: justified only when its tested filters or scheduling add value you cannot achieve with core tools. Review publisher, updates, capabilities, and deletion semantics first.

  • Direct SQL: not recommended for normal cleanup. Posts have metadata, taxonomy relationships, comments, caches, and plugin data that WordPress APIs know how to handle.

Before deletion: define the boundary

  • Write the target as a sentence: for example, “draft post records created before a reviewed date by one author.”

  • Confirm whether revisions, attachments, translations, products, orders, and plugin custom post types are explicitly in or out of scope.

  • Check legal retention, editorial holds, analytics needs, redirects, syndication, search indexing, and downstream integrations.

  • Pause content-generation/import jobs that could create or mutate records while the selection is being reviewed.

  • Make sure the backup includes the database and, when attachments are involved, the uploads/object-storage data.

Create a database rollback point

/var/www/example.combash
wp db export before-bulk-delete.sql
ls -lh before-bulk-delete.sql

What this backup proves—and what it does not

  • wp db export uses the active WordPress database configuration and writes an SQL dump without deleting content.

  • The size check catches an obvious zero-byte/missing export, but only a restore test in an isolated environment proves recoverability.

  • A database dump does not include files under wp-content/uploads, external object storage, server configuration, or secrets.

  • Store the export outside the public web root with restricted access; it may contain user and authentication-related data.

Dashboard method: reviewable and recoverable

  • Open Posts → All Posts, then use status, date, category, author, and search filters to narrow the table.

  • Open Screen Options and choose a manageable number of rows. Extremely large pages can hit PHP memory/time limits and make visual review unreliable.

  • Select the visible posts, choose Move to Trash under Bulk actions, and select Apply. Repeat page by page rather than assuming the header checkbox selected records beyond the current filtered view.

  • Open Trash immediately, check the count and several representative titles, then verify public URLs and dependent features.

  • Use Restore for mistakes. Choose Delete Permanently or Empty Trash only after the recovery window and review are complete.

WP-CLI method: preview before mutation

/var/www/example.combash
wp post list \
  --post_type=post \
  --post_status=draft \
  --fields=ID,post_title,post_date,post_status \
  --format=table

How the selector works

  • --post_type=post prevents pages and custom post types from joining the result accidentally.

  • --post_status=draft restricts the query to drafts. Adapt only one condition at a time and review the table after every change.

  • --fields keeps identity, title, date, and status visible so a human can spot an over-broad result.

  • wp post list reads only at this stage. Redirect the reviewed IDs to an access-controlled change record when auditability matters.

Move a reviewed set to Trash

/var/www/example.combash
wp post delete 123 456 789

Risk level: caution. Review the command before running it.

Replace every placeholder deliberately

  • 123 456 789 are examples, not literal production targets. Copy only IDs from the reviewed preview.

  • Without --force, wp post delete moves normal posts to Trash when that post type supports Trash.

  • WP-CLI invokes WordPress deletion APIs and related hooks; plugins may perform additional cleanup or remote actions. Test those effects on staging.

  • Save the command and output with the change ticket. A successful line for each ID is stronger evidence than assuming the batch completed.

Verify the post state after trashing

/var/www/example.combash
wp post list \
  --post__in=123,456,789 \
  --fields=ID,post_title,post_type,post_status \
  --format=table

What to verify beyond the command

  • The selected IDs should now report trash; unexpected types or statuses require investigation before any purge.

  • Visit representative old URLs while authenticated and logged out. Decide whether they should return 404/410 or redirect to a relevant maintained resource.

  • Check menus, related-content widgets, internal links, sitemaps, feeds, search indexes, API clients, and caches.

  • Monitor PHP/server logs and integration queues for hook failures triggered by the bulk operation.

Restore mistakes while Trash still exists

/var/www/example.combash
wp post update 123 456 --post_status=publish

Risk level: caution. Review the command before running it.

Restoration needs editorial review

  • wp post update changes the selected records back to the requested status through WordPress. Use their original statuses if they were not all published.

  • Restoring the database row may not reverse webhook notifications, cache invalidations, search removals, CDN changes, or third-party sync actions.

  • Verify permalinks, taxonomy, featured media, translations, and scheduled/publication dates after recovery.

  • For a widespread mistake, restoring the validated backup may be safer than reconstructing status one post at a time—coordinate the maintenance window first.

Permanently empty Trash in controlled batches

/var/www/example.combash
wp post list --post_type=post --post_status=trash --format=ids | \
  xargs -r -n 100 wp post delete --force --defer-term-counting

Risk level: destructive. Review the command before running it.

Why every flag matters

  • The left side selects only trashed records of the post type. Preview the same query as a table immediately before this command.

  • xargs -r -n 100 skips an empty input and submits batches of at most 100 IDs on GNU systems; confirm your host’s xargs semantics.

  • --force bypasses Trash and permanently deletes each selected record. Recovery then depends on backups and external systems.

  • --defer-term-counting postpones taxonomy recount work across each command batch for efficiency. Verify category/tag counts and relevant caches afterward.

Multisite, custom types, and scale

  • On multisite, add the exact --url=https://site.example/ target and print wp option get siteurl before mutation. A network install is not a single content pool.

  • Inspect registered post types and their Trash behavior; commerce/order and learning-management records may have business workflows far beyond wp_posts.

  • For very large sets, page through deterministic ID/date ranges and checkpoint each batch. Avoid a single shell expansion that can exceed argument limits or transaction/runtime boundaries.

  • Run under maintenance/change-control practices when deletion hooks invalidate expensive caches or call external services.

  • Afterward, rebuild only the caches/indexes documented by the active stack and verify database health; do not run generic “optimization” commands blindly.

Troubleshooting

  • Dashboard request times out: lower items per page and process smaller batches; inspect PHP/server logs for the actual limit.

  • WP-CLI targets the wrong site: stop, print wp option get home and siteurl, restore if necessary, then use explicit --path/--url.

  • Some IDs remain: inspect post type, status, permissions, plugin hooks, and WP-CLI output rather than rerunning a broader selector.

  • Trash vanished automatically: WordPress normally schedules trash cleanup according to EMPTY_TRASH_DAYS; hosting/plugins may change it. Treat Trash as temporary, not a backup.

  • URLs now generate search errors: restore valuable content or implement specific relevant redirects; never redirect every deleted URL to the homepage.

  • Counts look wrong: finish all batches, allow deferred term counting to complete, then run application-specific cache/index maintenance and verify again.

Primary references