Vai al contenuto principale

Cos’è la concorrenza

Se esegui in sequenza più operazioni indipendenti (chiamate ad API esterne, aggregazioni DB, ecc.), il tempo totale è la somma di ciascuna. Eseguendole in parallelo il tempo totale si riduce al tempo dell’operazione più lenta. La facade Concurrency di Laravel realizza questo con un’API semplice.
La facade Concurrency è stata introdotta in Laravel 11 ed è disponibile anche in Laravel 13. Il driver predefinito usa processi PHP figli e non richiede pacchetti aggiuntivi.

Come funziona

La facade Concurrency serializza le closure passate, le invia a un comando Artisan nascosto e le esegue come processi PHP separati. Al termine i valori di ritorno vengono serializzati e restituiti al processo genitore. Sono disponibili tre driver:
DriverDescrizione
processPredefinito. Avvia processi PHP figli. Funziona anche nelle richieste web.
forkRichiede il pacchetto spatie/fork. Fork del processo: solo CLI ma più veloce.
syncNessun parallelismo, esecuzione sequenziale. Utile per i test.

Uso di base

Concurrency::run()

Passa a run() un array di closure: verranno eseguite in parallelo. Ottieni i risultati come array nello stesso ordine delle closure.
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;

[$userCount, $orderCount] = Concurrency::run([
    fn () => DB::table('users')->count(),
    fn () => DB::table('orders')->count(),
]);

Scegliere il driver

Con driver() specifichi un driver.
$results = Concurrency::driver('fork')->run([
    fn () => fetchFromApiA(),
    fn () => fetchFromApiB(),
]);
Per cambiare il driver predefinito, pubblica la configurazione e aggiorna default.
php artisan config:publish concurrency

Driver fork

fork è più veloce di process ma disponibile solo in CLI (Artisan, queue worker). Non funziona nelle richieste web. Prima installa spatie/fork.
composer require spatie/fork
fork non funziona nelle richieste web. Usalo in comandi Artisan o worker.

Senza risultati: Concurrency::defer()

Se non ti interessa il risultato e vuoi eseguire dopo l’invio della risposta HTTP, usa defer().
use App\Services\Metrics;
use Illuminate\Support\Facades\Concurrency;

Concurrency::defer([
    fn () => Metrics::report('users'),
    fn () => Metrics::report('orders'),
]);
Le closure non vengono eseguite al momento della chiamata, ma dopo che la risposta è stata inviata.
defer() è ideale per raccolta di analytics o cache warming: attività che non richiedono di far attendere l’utente.

Esempio pratico: chiamare più API in parallelo

Dashboard di e-commerce che consulta tre API (inventario, vendite, spedizioni).

Sequenziale (prima)

use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    $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');
}

Parallela (dopo)

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

public function dashboard(): array
{
    [$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');
}
Con tre API da 1s ciascuna: sequenziale 3s, parallelo ~1s.

Aggregazioni DB in parallelo

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

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,
    ];
}
Con process ogni figlio apre una nuova connessione DB. Attenzione al limite di connessioni.

Nei test

Usa sync per esecuzione sequenziale, evitando l’overhead dei processi.
use Illuminate\Support\Facades\Concurrency;

public function test_dashboard_returns_correct_data(): void
{
    Concurrency::fake(); // Passa a driver sync

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

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

Note

Le closure vengono serializzate. Non catturare oggetti non serializzabili (connessioni DB, file handle) da fuori: costruiscili dentro la closure.
// NG
$user = User::find(1);
Concurrency::run([
    fn () => $user->orders()->count(),
]);

// OK
$userId = 1;
Concurrency::run([
    fn () => Order::where('user_id', $userId)->count(),
]);
process ha un costo di avvio dei figli: per operazioni molto brevi (< pochi ms) potresti non guadagnare. Utile per operazioni > 100ms.
Non funziona in FPM/Apache, solo in Artisan e worker.
Se in parallelo viene sollevata un’eccezione, run() la rilancia. Per continuare, gestiscila dentro ciascuna closure con try/catch.
Concurrency::run([
    function () {
        try {
            return Http::get('https://api.example.com/data')->json();
        } catch (\Exception $e) {
            return null;
        }
    },
]);

Prossimi passi

Code e job

Impara code e job per l’esecuzione asincrona in background.
Ultima modifica il 13 luglio 2026

Argomenti correlati

Cache