posts_per_page = -1 is a production incident waiting

It means unlimited, and it is fine on a development site with forty posts. The site it eventually runs on has forty thousand.

// the usual culprit — building a select for the admin
$all = get_posts( array( 'numberposts' => -1, 'post_type' => 'product' ) );

// what it should be
$ids = get_posts( array(
    'numberposts' => 500,
    'post_type'   => 'product',
    'fields'      => 'ids',      // no post objects, no meta priming
    'no_found_rows' => true,     // skips the SQL_CALC_FOUND_ROWS count
) );

fields => 'ids' is the flag that changes the cost most, because hydrating post objects also primes the meta and term caches for every one of them. no_found_rows removes a second full count that only matters if you are paginating. Where the requirement genuinely is every post — an export, a migration — the answer is batching by ID range, not a larger limit.