The query loop block puts a database query in the hands of an editor, with no indication of what it costs.
// the block's attributes become WP_Query arguments
// — including postsPerPage, taxQuery and order.
// so a filter is the only place to bound it
add_filter( 'query_loop_block_query_vars', function ( $q, $block ) {
$q['posts_per_page'] = min( (int) ( $q['posts_per_page'] ?? 10 ), 24 );
$q['no_found_rows'] = empty( $q['paged'] );
return $q;
}, 10, 2 );
An editor setting the count to 100 on a page with three query loops produces three hundred posts and their meta on one request, and nothing in the interface suggests that is expensive. Capping it in a filter is the only enforcement available. no_found_rows is worth setting when the block does not paginate, because the SQL_CALC_FOUND_ROWS that WordPress adds otherwise is a second full scan for a number nobody displays.