The WPFolks app is out on iPhone and Android. Free, no ads. Download on the App Store Get it on Google Play
Skip to main content
Login Join
Snippet · PHP

Permanently Delete Trashed Posts Older Than N Days

Shared by Jainil Nagar · June 15, 2026 · @wp_scheduled_delete

53 views
Back to Snippets

WordPress core empties the trash after 30 days but only via EMPTY_TRASH_DAYS constant — which is all-or-nothing and not per-post-type. This gives you per-post-type control with configurable retention and batched deletes.

/**
 * Permanently delete trashed posts older than a configurable number of days.
 * Runs daily via WP-Cron. Configurable per post type.
 *
 * Configure in wp-config.php:
 *   define( 'WPFOLKS_TRASH_RETENTION_DAYS', 14 ); // Default: 14 days
 *
 * Extend via filter:
 *   add_filter( 'wpfolks_trash_post_types', fn( $types ) => array_merge( $types, [ 'product' ] ) );
 */

defined( 'WPFOLKS_TRASH_RETENTION_DAYS' ) || define( 'WPFOLKS_TRASH_RETENTION_DAYS', 14 );

// ─── 1. Schedule daily cleanup ────────────────────────────────────────────────

add_action( 'init', 'wpfolks_schedule_trash_cleanup' );
function wpfolks_schedule_trash_cleanup(): void {
    if ( ! wp_next_scheduled( 'wpfolks_daily_trash_cleanup' ) ) {
        wp_schedule_event( time(), 'daily', 'wpfolks_daily_trash_cleanup' );
    }
}

// ─── 2. Run the cleanup ───────────────────────────────────────────────────────

add_action( 'wpfolks_daily_trash_cleanup', 'wpfolks_do_trash_cleanup' );
function wpfolks_do_trash_cleanup(): void {
    $days = absint( WPFOLKS_TRASH_RETENTION_DAYS );
    if ( $days < 1 ) return; // Safety: never delete with 0-day retention

    $post_types = apply_filters( 'wpfolks_trash_post_types', [ 'post', 'page' ] );
    $post_types = array_map( 'sanitize_key', (array) $post_types );

    if ( empty( $post_types ) ) return;

    $trashed_posts = new WP_Query( [
        'post_type'      => $post_types,
        'post_status'    => 'trash',
        'posts_per_page' => 50, // Batch — prevents memory exhaustion on large sites
        'date_query'     => [
            [
                'column' => 'post_modified_gmt',
                'before' => "{$days} days ago",
            ],
        ],
        'fields'         => 'ids',
        'no_found_rows'  => true, // Skip SQL_CALC_FOUND_ROWS for performance
    ] );

    if ( empty( $trashed_posts->posts ) ) return;

    foreach ( $trashed_posts->posts as $post_id ) {
        // wp_delete_post with true = force delete (bypass trash)
        $deleted = wp_delete_post( (int) $post_id, true );

        if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
            if ( $deleted ) {
                error_log( sprintf( '[WPFolks Trash] Permanently deleted post ID %d.', (int) $post_id ) );
            } else {
                error_log( sprintf( '[WPFolks Trash] Failed to delete post ID %d.', (int) $post_id ) );
            }
        }
    }
}

// ─── 3. Clean up cron event on deactivation ──────────────────────────────────

register_deactivation_hook( __FILE__, function (): void {
    wp_clear_scheduled_hook( 'wpfolks_daily_trash_cleanup' );
} );
Know a different way to do this? Add your approach as a variation so folks can compare them side by side.
Submit a variation

0 comments