Saltar al contenido principal

¿Qué es la concurrencia?

Cuando ejecutas en serie varios procesos que no dependen entre sí, como solicitudes a varias APIs externas o agregaciones en la base de datos, el tiempo total es la suma de los tiempos de cada proceso. Si los ejecutas de forma concurrente, el tiempo total se reduce al tiempo del proceso más lento. La fachada Concurrency de Laravel ofrece una API sencilla para lograr esa ejecución concurrente.
La fachada Concurrency se introdujo en Laravel 11 y sigue disponible en Laravel 13. Su driver por defecto usa procesos PHP hijos, por lo que funciona sin necesidad de paquetes adicionales.

Cómo funciona

La fachada Concurrency serializa los closures recibidos, los envía a un comando Artisan oculto y los ejecuta en procesos PHP separados. Cuando cada proceso termina, se serializa el valor devuelto y se retorna al proceso padre. Hay tres drivers disponibles.
DriverDescripción
processPor defecto. Lanza procesos PHP hijos. Funciona también en solicitudes web
forkRequiere el paquete spatie/fork. Como hace fork del proceso, solo funciona en CLI, pero es rápido
syncNo ejecuta en paralelo, sino en secuencia. Adecuado para pruebas

Uso básico

Concurrency::run()

Al pasar un array de closures a run(), se ejecutan concurrentemente. El valor devuelto es un array con los valores retornados por cada 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(),
]);
Con la desestructuración de arrays puedes recibir cada resultado en una variable. El orden de los closures coincide con el orden de los valores devueltos.

Especificar el driver

Si quieres usar un driver específico, indícalo con el método driver().
$results = Concurrency::driver('fork')->run([
    fn () => fetchFromApiA(),
    fn () => fetchFromApiB(),
]);
Para cambiar el driver por defecto, publica el archivo de configuración y actualiza la opción default.
php artisan config:publish concurrency

Uso del driver fork

El driver fork es más rápido que process, pero solo puede usarse en un entorno CLI de PHP (comandos Artisan o workers de cola). No puede usarse dentro de una solicitud web. Antes de utilizarlo, instala el paquete spatie/fork.
composer require spatie/fork
El driver fork no funciona durante una solicitud web. Úsalo cuando quieras concurrencia dentro de comandos Artisan o workers de cola.

Sin recibir resultados: Concurrency::defer()

Si no te interesa el resultado del proceso y quieres ejecutarlo en segundo plano después de haber devuelto la respuesta HTTP, usa el método defer().
use App\Services\Metrics;
use Illuminate\Support\Facades\Concurrency;

Concurrency::defer([
    fn () => Metrics::report('users'),
    fn () => Metrics::report('orders'),
]);
En el momento de llamar a defer() los closures no se ejecutan. Se ejecutan concurrentemente después de que la respuesta HTTP se haya enviado al usuario.
defer() es ideal para procesos que no deben hacer esperar al usuario, como registrar datos analíticos o precalentar cachés.

Ejemplo práctico: llamar a varias APIs externas a la vez

Piensa en un dashboard de un ecommerce que consume tres APIs: gestión de inventario, ventas y estado de envíos.

Ejecución en serie (antes)

use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    // Espera a que cada solicitud termine, una tras otra (total ~3 s)
    $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');
}

Ejecución concurrente (después)

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

public function dashboard(): array
{
    // Ejecuta las tres solicitudes en paralelo (termina en el tiempo de la API más lenta)
    [$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 cada API tarda 1 segundo, la ejecución en serie tarda 3 segundos, mientras que la concurrente termina en aproximadamente 1 segundo.

Ejecutar varias agregaciones de base de datos a la vez

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,
    ];
}
Al usar el driver process para agregaciones de base de datos, cada proceso hijo establece una nueva conexión. Con muchos procesos en paralelo, presta atención al número máximo de conexiones de la base de datos.

Configuración para pruebas

En el entorno de pruebas conviene usar el driver sync, que ejecuta los closures uno tras otro. Evitas el coste de arrancar procesos concurrentes y las pruebas se hacen más rápidas.
// tests/Feature/DashboardTest.php
use Illuminate\Support\Facades\Concurrency;

public function test_dashboard_returns_correct_data(): void
{
    Concurrency::fake(); // Cambia al driver sync

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

    $response->assertStatus(200);
}
Otra opción es cambiar el driver por defecto en .env.testing.
CONCURRENCY_DRIVER=sync

Advertencias

Los closures se serializan y se pasan al proceso hijo. No puedes capturar del exterior objetos no serializables (conexiones a base de datos, handles de archivo, recursos, etc.). Crea dentro del closure los objetos que necesites.
// Mal: captura un modelo Eloquent externo
$user = User::find(1);
Concurrency::run([
    fn () => $user->orders()->count(), // Podría no ser serializable
]);

// Bien: obtener los datos dentro del closure
$userId = 1;
Concurrency::run([
    fn () => Order::where('user_id', $userId)->count(),
]);
El driver process tiene el coste de arrancar procesos hijos, así que para operaciones muy cortas (unos pocos milisegundos) puede no ser más rápido. Aporta beneficios cuando se paralelizan operaciones que tardan más de 100 ms, como solicitudes HTTP o agregaciones de base de datos.
Como fork hace fork del proceso PHP, no puede usarse durante una solicitud web (FPM o Apache). Úsalo solo dentro de comandos Artisan o workers de cola.
Si durante la ejecución concurrente se lanza una excepción, run() la vuelve a lanzar. Si quieres que el resto de operaciones continúen aunque una lance excepción, usa try/catch dentro del closure.
Concurrency::run([
    function () {
        try {
            return Http::get('https://api.example.com/data')->json();
        } catch (\Exception $e) {
            return null; // Devuelve null en caso de fallo
        }
    },
]);

Próximos pasos

Colas y jobs

Aprende a ejecutar procesos en segundo plano de forma asíncrona con colas y jobs
Última modificación el 13 de julio de 2026

Temas relacionados

Caché