Concurrency en Laravel: Ejecuta Tareas Paralelas sin Dolor
Introducción
Uno de los mayores desafíos en desarrollo web es manejar múltiples operaciones que dependen unas de otras sin bloquear la ejecución. Laravel 13 introduce Concurrency Manager, una herramienta poderosa que permite ejecutar tareas en paralelo de manera sencilla y elegante.
Si alguna vez has necesitado:
- Llamar a múltiples APIs externas al mismo tiempo
- Procesar archivos en paralelo
- Consultar varias bases de datos simultáneamente
- Esperar que múltiples operaciones terminen antes de continuar
Entonces este artículo es para ti. Veremos cómo usar Concurrency de forma práctica y cómo evitar los errores más comunes.
¿Qué es Concurrency en Laravel?
Concurrency Manager es un patrón que te permite ejecutar múltiples tareas de forma asincrónica sin necesidad de usar colas o WebSockets. Es perfecto para operaciones síncronas que necesitan paralelismo temporal.
Diferencia importante:
- Queues/Jobs: Para tareas que no necesitan respuesta inmediata
- Concurrency: Para ejecutar múltiples operaciones y obtener resultados antes de continuar
Es como la diferencia entre enviar emails en background (colas) vs esperar respuestas de múltiples APIs al mismo tiempo (concurrency).
Instalación y Configuración Básica
Concurrency viene incluido en Laravel 13. Si vienes de una versión anterior, asegúrate de actualizar:
composer update laravel/framework
No requiere configuración adicional. Está listo para usar inmediatamente en tus controladores, jobs o comandos.
Tu Primer Ejemplo: Concurrency Básico
Imagina que necesitas obtener información de usuario desde múltiples servicios:
<?php
namespace App\Http\Controllers;
use Illuminate\Concurrency\ConcurrencyManager;
use Illuminate\Support\Facades\Http;
class UserProfileController extends Controller
{
public function show($userId)
{
$manager = new ConcurrencyManager();
// Ejecutar múltiples tareas en paralelo
[$user, $posts, $comments] = $manager->run([
fn() => $this->fetchUser($userId),
fn() => $this->fetchPosts($userId),
fn() => $this->fetchComments($userId),
]);
return view('profile', [
'user' => $user,
'posts' => $posts,
'comments' => $comments,
]);
}
private function fetchUser($userId)
{
return Http::get("https://api.example.com/users/{$userId}")->json();
}
private function fetchPosts($userId)
{
return Http::get("https://api.example.com/users/{$userId}/posts")->json();
}
private function fetchComments($userId)
{
return Http::get("https://api.example.com/users/{$userId}/comments")->json();
}
}
¿Qué sucede aquí?
ConcurrencyManager::run()recibe un array de closures- Ejecuta todas las tareas en paralelo
- Espera a que todas terminen
- Retorna los resultados en el mismo orden
Sin concurrency, esto tomaría ~3 segundos (suma de todas las peticiones). Con concurrency, toma solo ~1 segundo (el tiempo de la petición más lenta).
Manejo de Errores en Operaciones Paralelas
Cuando trabajas con múltiples operaciones, los errores se vuelven críticos. Veamos cómo manejarlos:
Opción 1: Try-Catch Dentro de Cada Closure
$results = $manager->run([
fn() => try {
return Http::timeout(5)->get('https://api1.example.com/data')->json();
} catch (Exception $e) {
return ['error' => $e->getMessage()];
},
fn() => try {
return Http::timeout(5)->get('https://api2.example.com/data')->json();
} catch (Exception $e) {
return ['error' => $e->getMessage()];
},
]);
// Procesar resultados
foreach ($results as $result) {
if (isset($result['error'])) {
Log::warning('API Error: ' . $result['error']);
}
}
Opción 2: Usar Métodos Helper
class ApiService
{
public function safeApiCall($url, $timeout = 5)
{
try {
return Http::timeout($timeout)->get($url)->json();
} catch (\Exception $e) {
report($e);
return null;
}
}
}
// En el controlador
$service = app(ApiService::class);
$results = $manager->run([
fn() => $service->safeApiCall('https://api1.example.com/data'),
fn() => $service->safeApiCall('https://api2.example.com/data'),
fn() => $service->safeApiCall('https://api3.example.com/data'),
]);
Caso Real: Procesamiento de Múltiples Archivos
Supongamos que necesitas procesar varias imágenes y generar thumbnails en paralelo:
<?php
namespace App\Http\Controllers;
use Illuminate\Concurrency\ConcurrencyManager;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
class ImageProcessingController extends Controller
{
public function processImages($productId)
{
$files = request()->file('images');
$manager = new ConcurrencyManager();
// Crear array de closures para procesar cada imagen
$processingTasks = collect($files)->map(function ($file, $index) {
return fn() => $this->processImage($file, $productId, $index);
})->toArray();
// Ejecutar todo en paralelo
$results = $manager->run($processingTasks);
// Guardar información en base de datos
foreach ($results as $result) {
if ($result['success']) {
Product::find($result['product_id'])->images()->create([
'path' => $result['path'],
'thumbnail_path' => $result['thumbnail_path'],
]);
}
}
return response()->json(['processed' => count($results)]);
}
private function processImage($file, $productId, $index)
{
try {
$originalPath = "products/{$productId}/images/{$index}.jpg";
$thumbnailPath = "products/{$productId}/thumbnails/{$index}.jpg";
// Procesar imagen original
$image = Image::make($file);
Storage::put($originalPath, $image->encode('jpg', 80));
// Generar thumbnail
$thumbnail = $image->resize(200, 200);
Storage::put($thumbnailPath, $thumbnail->encode('jpg', 80));
return [
'success' => true,
'product_id' => $productId,
'path' => $originalPath,
'thumbnail_path' => $thumbnailPath,
];
} catch (\Exception $e) {
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
}
Combinando Concurrency con Queues
A veces necesitas lo mejor de ambos mundos: procesar múltiples operaciones en paralelo y ejecutarlas en background.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Concurrency\ConcurrencyManager;
class ProcessReportJob implements ShouldQueue
{
use Queueable;
public function __construct(public int $reportId) {}
public function handle()
{
$manager = new ConcurrencyManager();
// Ejecutar múltiples subcálculos en paralelo
[$sales, $expenses, $revenue] = $manager->run([
fn() => $this->calculateSales(),
fn() => $this->calculateExpenses(),
fn() => $this->calculateRevenue(),
]);
// Guardar resultado consolidado
Report::find($this->reportId)->update([
'sales' => $sales,
'expenses' => $expenses,
'revenue' => $revenue,
'processed_at' => now(),
]);
}
private function calculateSales()
{
return Order::where('status', 'completed')
->sum('total');
}
private function calculateExpenses()
{
return Expense::sum('amount');
}
private function calculateRevenue()
{
return Invoice::where('paid', true)
->sum('amount');
}
}
Limitaciones y Consideraciones Importantes
1. Concurrency != Multithreading Real
Laravel’s Concurrency no crea threads del sistema operativo. Es una forma elegante de orquestar operaciones I/O bound (HTTP calls, database queries) de forma no bloqueante.
// ❌ Esto NO será más rápido con concurrency
$manager->run([
fn() => expensiveCalculation(), // CPU-bound
fn() => expensiveCalculation(), // CPU-bound
]);
// ✅ Esto SÍ será más rápido
$manager->run([
fn() => Http::get('https://api1.com'), // I/O-bound
fn() => Http::get('https://api2.com'), // I/O-bound
]);
2. Timeouts y Limites
Por defecto, no hay timeout global. Debes configurar timeouts en cada operación:
$manager->run([
fn() => Http::timeout(10)->get('url1'),
fn() => Http::timeout(10)->get('url2'),
]);
3. Uso de Memoria
Ejecutar muchas operaciones pesadas en paralelo puede consumir mucha memoria. Siempre monitorea:
$heavyTasks = [];
for ($i = 0; $i < 100; $i++) {
$heavyTasks[] = fn() => $this->processLargeDataset($i);
}
// Dividir en chunks si es necesario
collect($heavyTasks)->chunk(10)->each(function ($chunk) {
$manager->run($chunk->toArray());
});
Comparativa: Antes vs Después
// ❌ ANTES: Secuencial (3 segundos)
$start = microtime(true);
$user = Http::get('https://api.example.com/user/1')->json();
$posts = Http::get('https://api.example.com/posts/user/1')->json();
$comments = Http::get('https://api.example.com/comments/user/1')->json();
$duration = microtime(true) - $start; // ~3s
// ✅ DESPUÉS: Paralelo (1 segundo)
$start = microtime(true);
$manager = new ConcurrencyManager();
[$user, $posts, $comments] = $manager->run([
fn() => Http::get('https://api.example.com/user/1')->json(),
fn() => Http::get('https://api.example.com/posts/user/1')->json(),
fn() => Http::get('https://api.example.com/comments/user/1')->json(),
]);
$duration = microtime(true) - $start; // ~1s
Testing de Código con Concurrency
Al testear, necesitas asegurarte de que tus operaciones paralelas funcionan correctamente:
<?php
namespace Tests\Feature;
use Illuminate\Concurrency\ConcurrencyManager;
use Tests\TestCase;
class ConcurrencyTest extends TestCase
{
public function test_concurrent_operations_complete_successfully()
{
$manager = new ConcurrencyManager();
$results = $manager->run([
fn() => 1 + 1,
fn() => 2 + 2,
fn() => 3 + 3,
]);
$this->assertEquals([2, 4, 6], $results);
}
public function test_concurrent_api_calls_handle_errors()
{
$manager = new ConcurrencyManager();
// Usar Http::fake() para simular respuestas
Http::fake([
'api1.com/*' => Http::response(['status' => 'ok']),
'api2.com/*' => Http::response(null, 500),
]);
$results = $manager->run([
fn() => try {
return Http::get('https://api1.com/data')->json();
} catch (\Exception $e) {
return null;
},
fn() => try {
return Http::get('https://api2.com/data')->json();
} catch (\Exception $e) {
return null;
},
]);
$this->assertNotNull($results[0]);
$this->assertNull($results[1]);
}
}
Puntos Clave
- Concurrency Manager permite ejecutar múltiples tareas en paralelo y obtener resultados consolidados
- Es ideal para operaciones I/O-bound (HTTP calls, database queries), no para cálculos CPU-intensivos
- Siempre envuelve operaciones en try-catch para manejar errores de forma elegante
- Configura timeouts explícitos en cada operación para evitar bloqueos indefinidos
- Divide tareas pesadas en chunks si vas a procesar cientos de operaciones
- Usa concurrency dentro de jobs para combinar paralelismo con procesamiento en background
- Monitorea memoria cuando ejecutas muchas operaciones simultáneamente
- En tests, usa
Http::fake()para simular respuestas y validar comportamiento - Recuerda que no es multithreading real, sino orquestación eficiente de I/O