Passer au contenu principal

Qu’est-ce que la concurrence

Exécuter séquentiellement plusieurs traitements indépendants (appels d’API externes, agrégations DB…) additionne les durées. En les exécutant en parallèle, la durée totale se réduit à celle du plus long. La façade Concurrency de Laravel fournit une API simple pour cela.
Introduite avec Laravel 11, elle reste disponible sous Laravel 13. Le driver par défaut utilise des sous-processus PHP, sans package additionnel.

Fonctionnement

Concurrency sérialise chaque closure et l’envoie à une commande Artisan interne exécutée dans un processus séparé. Les valeurs de retour sont sérialisées et renvoyées au processus parent. Trois drivers :
DriverDescription
processPar défaut. Sous-processus PHP. Fonctionne en Web
forkNécessite spatie/fork. Uniquement en CLI, plus rapide
syncSéquentiel. Utile pour les tests

Utilisation

Concurrency::run()

Passez un tableau de closures. Elles s’exécutent en parallèle, et vous récupérez leurs retours dans le même ordre.
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;

[$userCount, $orderCount] = Concurrency::run([
    fn () => DB::table('users')->count(),
    fn () => DB::table('orders')->count(),
]);
Destructuration du tableau pour affecter chaque résultat.

Choisir un driver

Avec driver() :
$results = Concurrency::driver('fork')->run([
    fn () => fetchFromApiA(),
    fn () => fetchFromApiB(),
]);
Pour changer le driver par défaut, publiez la configuration.
php artisan config:publish concurrency

Utiliser le driver fork

Plus rapide que process, mais uniquement en environnement CLI (Artisan, workers). Non utilisable dans une requête web. Installez spatie/fork d’abord.
composer require spatie/fork
fork ne fonctionne pas dans une requête web. Utilisez-le dans une commande Artisan ou un worker de queue.

Ignorer les résultats : Concurrency::defer()

Si les résultats n’importent pas et que le traitement peut s’exécuter après la réponse HTTP, utilisez defer().
use App\Services\Metrics;
use Illuminate\Support\Facades\Concurrency;

Concurrency::defer([
    fn () => Metrics::report('users'),
    fn () => Metrics::report('orders'),
]);
L’exécution démarre après l’envoi de la réponse, en parallèle.
Idéal pour les métriques, les warmups de cache, etc., quand l’utilisateur n’a pas à attendre.

Exemple : appeler plusieurs API en parallèle

Dashboard e-commerce interrogeant l’inventaire, les ventes et l’expédition.

Séquentiel (avant)

use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    // Chaque requête attend la précédente (~3 s au total)
    $inventory = Http::get('https://api.example.com/inventory')->json();
    $sales     = Http::get('https://api.example.com/sales')->json();
    $shipping  = Http::get('https://api.example.com/shipping')->json();

    return compact('inventory', 'sales', 'shipping');
}

Parallèle (après)

use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    // Les 3 requêtes en parallèle (durée = la plus lente)
    [$inventory, $sales, $shipping] = Concurrency::run([
        fn () => Http::get('https://api.example.com/inventory')->json(),
        fn () => Http::get('https://api.example.com/sales')->json(),
        fn () => Http::get('https://api.example.com/shipping')->json(),
    ]);

    return compact('inventory', 'sales', 'shipping');
}
Si chaque API met 1 s : ~3 s en séquentiel, ~1 s en parallèle.

Plusieurs agrégations DB en parallèle

use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;

public function statistics(): array
{
    [$totalUsers, $activeUsers, $totalOrders, $revenue] = Concurrency::run([
        fn () => DB::table('users')->count(),
        fn () => DB::table('users')->where('active', true)->count(),
        fn () => DB::table('orders')->count(),
        fn () => DB::table('orders')->sum('total_amount'),
    ]);

    return [
        'total_users'  => $totalUsers,
        'active_users' => $activeUsers,
        'total_orders' => $totalOrders,
        'revenue'      => $revenue,
    ];
}
Chaque sous-processus ouvre sa propre connexion DB. Attention au nombre max de connexions concurrentes.

Configuration pour les tests

Le driver sync exécute séquentiellement — pas de coût de sous-processus, tests plus rapides.
// tests/Feature/DashboardTest.php
use Illuminate\Support\Facades\Concurrency;

public function test_dashboard_returns_correct_data(): void
{
    Concurrency::fake();

    $response = $this->get('/dashboard');

    $response->assertStatus(200);
}
Ou dans .env.testing :
CONCURRENCY_DRIVER=sync

Précautions

Les closures sont sérialisées puis passées au sous-processus. Vous ne pouvez pas capturer d’objets non sérialisables (connexions DB, ressources). Recréez les objets nécessaires à l’intérieur de la closure.
// NG : capture un modèle Eloquent
$user = User::find(1);
Concurrency::run([
    fn () => $user->orders()->count(), // Peut ne pas être sérialisable
]);

// OK : reconstruit à l'intérieur
$userId = 1;
Concurrency::run([
    fn () => Order::where('user_id', $userId)->count(),
]);
Le démarrage des sous-processus a un coût. Pour des traitements très rapides (< qq ms), la parallélisation peut être plus lente. Elle est efficace pour des tâches > 100 ms (HTTP, agrégations).
fork clone le processus PHP : impossible en web (FPM, Apache). Utilisez-le en Artisan / worker.
Toute exception se propage via run(). Pour laisser les autres closures s’exécuter, gérez try/catch dans chaque closure.
Concurrency::run([
    function () {
        try {
            return Http::get('https://api.example.com/data')->json();
        } catch (\Exception $e) {
            return null;
        }
    },
]);

Prochaines étapes

Files d'attente et jobs

Découvrez comment exécuter des traitements en arrière-plan.
Dernière modification le 13 juillet 2026

Sujets connexes

ConcurrenceClient HTTP