wp_redirect leading to an infinite loop

You’ve got most of it right. However, there is one little caveat: $url doesn’t mean anything in your context. You can easily prove that by using something like this for debugging

add_action('template_redirect', 'post_redirect_by_custom_filters');
function post_redirect_by_custom_filters() {
    var_dump($url);
    wp_die();
}

So all that is left for your code to work is actually defining $url as the requested URL (or at least the path):

add_action('template_redirect', 'post_redirect_by_custom_filters');
function post_redirect_by_custom_filters() {
    global $post;

    // this array can contain category names, slugs or even IDs.
    $catArray = ['grill-rezepte','Test2'];

    // get current path
    $url = $_SERVER['REQUEST_URI']; 

    // slug is already in URL, return early
    if (strpos($url,'grill-rezept') !== false) {
        return;
    }

    if (is_single($post->ID) && has_category($catArray, $post)) {
        $new_url = "https://bbqpit.de/grill-rezepte/{$post->post_name}/";  
        wp_redirect($new_url, 301);
        exit;
    }
}