posts_per_page = -1 is an 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 an admin screen
$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 objects, no meta priming
    'no_found_rows' => true,       // skips the second count query
) );

fields => 'ids' 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 full count that only matters when paginating. Where the requirement genuinely is every post — an export, a migration — the answer is batching by ID range rather than a larger limit, and doing it in WP-CLI rather than in a request.