Si tu equipo siente que el software ya no escala al ritmo que necesita el negocio, el paralelismo suele aparecer como la solución obvia. El problema es que muchas veces se aborda como una receta mágica: “metamos hilos”, “dividamos todo”, “hagámoslo async”. Ahí es donde empiezan los bugs raros, los tiempos de espera impredecibles y una base de código que cuesta más mantener que la ganancia real que entrega.
La idea útil no es paralelizar por deporte. La idea útil es pensar el trabajo como una secuencia de costos, dependencias y límites. Cuando lo ves así, puedes decidir qué conviene ejecutar al mismo tiempo, qué conviene dejar serial, y qué conviene ni tocar porque el cuello de botella está en otro lado. Esa es la guía mental del paralelismo: menos intuición heroica, más criterio operativo.
Cambia la pregunta: no es “¿cómo paralelizo?”, es “¿qué me está frenando?”
Antes de abrir un issue para meter threads, worker pools o colas, conviene responder una pregunta más incómoda: ¿dónde se va el tiempo? Muchas veces el problema no es CPU. Puede ser red, disco, una query lenta, un lock, una dependencia externa o una combinación de todo eso. Si paralelizas sin medir, solo distribuyes el caos.
Un ejemplo simple: si una API tarda 900 ms y 700 ms de ese tiempo son una consulta a la base de datos, meter más paralelismo en el handler no va a arreglar gran cosa. En cambio, si tu endpoint hace 8 llamadas independientes de 120 ms cada una y hoy las ejecuta en serie, ahí sí tienes una oportunidad clara: bajar el tiempo total cerca del máximo de esas llamadas, no de la suma. Esa diferencia cambia por completo la decisión técnica.
La forma práctica de pensar esto es separar el trabajo en tres grupos:
- Trabajo independiente que puede correr al mismo tiempo.
- Trabajo dependiente que debe esperar un resultado anterior.
- Trabajo que parece paralelizable, pero en realidad está limitado por un recurso compartido.
Si no haces esa separación, terminas con soluciones “más concurrentes” pero no necesariamente más rápidas. Y más rápido es el objetivo, no más concurrencia por sí misma.
Mide primero, decide después
No necesitas una plataforma enorme de observabilidad para empezar. Necesitas una línea base. Con una medición simple ya puedes tomar mejores decisiones: tiempo total, percentiles, CPU, memoria, número de llamadas externas y tiempo de espera por recurso. Si no tienes eso, estás adivinando.
En equipos pequeños o medianos, una práctica útil es registrar tres números por flujo crítico: tiempo promedio, p95 y cantidad de operaciones por request. Con eso ya detectas patrones. Por ejemplo, un endpoint puede tener 200 ms promedio, pero p95 de 2.5 s por una dependencia externa inestable. Ahí el problema no es “falta de paralelismo”, sino resiliencia y aislamiento.
Para medición real, puedes apoyarte en herramientas oficiales del stack que uses. Si trabajas con Go, la documentación de pprof es un buen punto de partida: https://pkg.go.dev/net/http/pprof. Si trabajas con Node.js, revisa worker_threads en la documentación oficial: https://nodejs.org/api/worker_threads.html. El punto no es la herramienta, sino el hábito de medir antes de tocar la arquitectura.
El paralelismo útil tiene forma de árbol, no de explosión
La mayoría de los sistemas sanos no paralelizan “todo”. Paralelizan partes concretas del trabajo y luego vuelven a juntar resultados. Piensa en un árbol: una petición entra, se divide en tareas independientes, cada rama avanza sola, y al final se consolidan los resultados. Eso es más fácil de razonar que una malla donde todo habla con todo.
Esa forma mental te ayuda a evitar dos problemas clásicos. El primero es la sobreparalelización: crear más workers de los necesarios y gastar tiempo en coordinación. El segundo es la falsa independencia: asumir que dos tareas no se afectan cuando en realidad comparten caché, lock, conexión o cuota externa.
Paralelismo no es lo mismo que concurrencia
La concurrencia es una forma de organizar tareas para que progresen sin bloquearse entre sí. El paralelismo es ejecutar varias tareas literalmente al mismo tiempo, normalmente en varios núcleos o procesos. Puedes tener concurrencia sin paralelismo, y también paralelismo sin una buena estrategia de concurrencia.
En práctica, esto importa porque muchas decisiones de diseño se confunden. Un sistema con async/await puede ser concurrente, pero si todas las operaciones dependen de una sola base de datos saturada, no vas a ver una mejora lineal. Del mismo modo, un job que usa 8 procesos para comprimir archivos puede ser paralelo, pero si cada proceso compite por el mismo disco, el rendimiento puede empeorar.
La regla útil es esta: primero identifica independencia real, después decide la forma de ejecución. No al revés.
Un criterio simple: divide por dependencia, no por tamaño
Mucha gente divide el trabajo en partes iguales porque suena ordenado. Pero el tamaño no es el mejor criterio. El mejor criterio es la dependencia. Si una tarea necesita el resultado de otra, no gana nada con ir en paralelo.
Imagina que generas un reporte con tres pasos: extraer datos, limpiar datos y renderizar PDF. La extracción puede tardar 400 ms, la limpieza 150 ms y el PDF 300 ms. Si la limpieza depende de la extracción y el PDF depende de la limpieza, no hay mucho paralelismo real dentro de esa cadena. En cambio, si el reporte incluye 10 secciones independientes, puedes procesarlas en paralelo y luego unirlas.
Esa distinción evita una trampa común: fragmentar demasiado una tarea secuencial. Más fragmentos no significan más velocidad. A veces solo significan más coordinación.
Dónde sí conviene paralelizar
Hay casos donde el beneficio es claro y medible. El más obvio es cuando tienes tareas independientes y relativamente costosas. Por ejemplo, consultar varias fuentes externas, procesar archivos separados, calcular métricas por segmento o transformar lotes de datos que no se pisan entre sí.
Otro caso frecuente está en pipelines de backend. Si tu sistema recibe 100 imágenes y cada una se redimensiona y comprime de forma aislada, puedes distribuir ese trabajo entre varios workers. Lo mismo aplica a análisis de logs, generación de thumbnails, envío de notificaciones o validación de documentos.
La clave es que el paralelismo debe reducir el tiempo de espera percibido o aumentar el throughput sin romper la simplicidad del sistema. Si solo reduce unos pocos milisegundos pero añade colas, retries y debugging difícil, probablemente no valga la pena.
Casos típicos con buena relación costo-beneficio
Aquí tienes ejemplos donde suele haber retorno claro:
- Procesar archivos independientes en lote, como 1,000 imágenes o 500 PDFs.
- Consultar varias APIs externas que no dependen entre sí.
- Ejecutar validaciones que no comparten estado mutable.
- Calcular agregaciones por usuario, por tienda o por región.
- Generar vistas previas, miniaturas o reportes en segundo plano.
En todos esos casos, el truco no es “paralelizar todo”, sino limitar el grado de paralelismo. Si lanzas 500 tareas al mismo tiempo, puedes saturar CPU, memoria, red o la API destino. Un pool de 4, 8 o 16 workers suele ser más razonable que una explosión sin control, aunque el número exacto depende de tu carga y de tu infraestructura.
Casos donde el paralelismo suele engañar
Hay escenarios donde la ganancia es menor de lo que parece. Uno es el procesamiento con muchos locks. Otro es cuando la operación principal depende de una base de datos que ya está cerca del límite. También pasa cuando el trabajo es tan pequeño que el overhead de coordinarlo supera el beneficio.
Por ejemplo, paralelizar una operación que tarda 5 ms cada una puede salir mal si el costo de crear tareas, sincronizar resultados y manejar errores supera ese tiempo. En ese caso, el sistema se vuelve más complejo sin mejorar la experiencia del usuario.
También hay que desconfiar de los cuellos de botella compartidos. Si 20 workers escriben al mismo archivo, al mismo índice o a la misma fila, el paralelismo se convierte en cola con más pasos intermedios. No es escalamiento, es congestión.
Cómo diseñarlo sin perder control
La forma más segura de introducir paralelismo es hacerlo por capas. Primero aislas el trabajo independiente. Después defines el límite de concurrencia. Luego agregas observabilidad y manejo de errores. Por último, evalúas si la mejora justifica la complejidad extra.
Este enfoque evita el clásico “big bang refactor” donde todo cambia a la vez. Si el resultado sale mal, no sabes si falló la partición, la coordinación, el límite de workers o la dependencia externa. Con cambios graduales, puedes comparar antes y después con números concretos.
Una secuencia práctica de implementación
- Identifica el flujo más lento y mide su tiempo actual.
- Separa tareas independientes de tareas dependientes.
- Define un límite de paralelismo inicial, por ejemplo 4 u 8 workers.
- Agrega timeout y cancelación para no dejar tareas huérfanas.
- Mide CPU, memoria, latencia y errores antes de subir la carga.
- Ajusta el límite según el cuello real, no según una intuición.
Si estás en Go, por ejemplo, puedes usar goroutines con un semáforo o un worker pool para controlar el grado de paralelismo. Si estás en Node.js, worker_threads sirve para cómputo intensivo, mientras que para I/O muchas veces basta con modelar bien la asincronía. La documentación oficial de Node lo explica bien: https://nodejs.org/api/worker_threads.html.
Tabla de decisión rápida
| Situación | ¿Paralelizar? | Motivo |
|---|---|---|
| 8 llamadas HTTP independientes de 120 ms | Sí | Reduce el tiempo total al máximo de las llamadas, no a la suma |
| 1 query lenta que usa el 90% del tiempo | No todavía | El cuello está en la base de datos, no en el scheduler |
| 500 archivos independientes | Sí | Escala bien con workers limitados |
| Operación de 5 ms con mucha coordinación | No | El overhead puede ser mayor que la ganancia |
| Proceso con lock compartido | Con cuidado | El lock puede anular el beneficio |
Manejo de errores y cancelación
Cuando paralelizas, los errores dejan de ser lineales. Una tarea puede fallar mientras otras siguen corriendo. Si no defines qué pasa en ese caso, terminas con trabajo desperdiciado, datos parciales o respuestas inconsistentes.
Por eso conviene decidir desde el inicio si el flujo es “todo o nada” o “me quedo con lo que alcance”. En un proceso de facturación, probablemente quieras abortar si una parte crítica falla. En una generación de previews, quizá puedas devolver 9 de 10 resultados y reintentar el faltante después. Esa decisión no es técnica solamente; también es de producto.
Un modelo mental para equipos: costo, aislamiento y retorno
Si tuviera que resumir la guía mental en tres preguntas, serían estas: ¿cuánto cuesta cada tarea?, ¿qué tan aislada está?, ¿qué retorno real me da paralelizarla? Con esas tres preguntas puedes filtrar la mayoría de las ideas malas antes de escribir código.
Costo significa tiempo, CPU, memoria o recursos externos. Aislamiento significa que la tarea no pisa estado compartido ni depende de un resultado previo. Retorno significa impacto visible en latencia, throughput o estabilidad. Si una iniciativa solo mejora una métrica de laboratorio pero complica el despliegue, no es una buena inversión.
Cómo lo discutimos en una revisión técnica
En una revisión útil, no basta con decir “esto se puede hacer en paralelo”. Hay que responder cosas más concretas:
- ¿Cuál es el cuello de botella actual?
- ¿Qué parte del flujo es independiente?
- ¿Cuántas tareas simultáneas soporta el sistema sin degradarse?
- ¿Qué pasa si una tarea falla a mitad del lote?
- ¿Qué métrica mejora y cuánto?
Ese tipo de conversación ahorra semanas. También reduce el riesgo de que un cambio “optimizado” termine afectando la estabilidad de producción.
Ejemplo realista de decisión
Supón que tu equipo en Ecuador tiene un servicio que genera reportes diarios para 2,000 clientes. Hoy el proceso tarda 18 minutos y la mayoría del tiempo se va en transformar datos y renderizar archivos, no en consultar la base. Ahí sí tiene sentido dividir el trabajo por cliente o por lote pequeño, con un límite de workers para no saturar el disco ni la CPU.
En cambio, si el mismo proceso ya baja a 4 minutos después de optimizar la consulta principal, quizá el paralelismo extra solo te ahorre 40 segundos y te complique el soporte. En ese punto, la decisión ya no es técnica pura. Es costo-beneficio.
Tabla resumen
| Pregunta | Respuesta corta |
|---|---|
| ¿Cuándo conviene paralelizar? | Cuando hay tareas independientes y el cuello no está en un recurso compartido |
| ¿Qué debes medir primero? | Tiempo total, p95, CPU, memoria y dependencias externas |
| ¿Qué límite usar? | Uno razonable, como 4, 8 o 16 workers, según el caso |
| ¿Qué error evitar? | Paralelizar trabajo pequeño o muy acoplado |
| ¿Qué importa más que la técnica? | El retorno real frente a la complejidad añadida |
| ¿Qué pasa si falla una tarea? | Debes definir si abortas todo o si sigues con resultados parciales |
Si quieres pensar mejor el paralelismo, quédate con esta idea: no se trata de repartir trabajo a ciegas, sino de encontrar independencia real y proteger el sistema de la complejidad innecesaria. Eso te permite acelerar sin romper mantenimiento, algo que casi siempre vale más que una mejora bonita en un benchmark.
Preguntas frecuentes
¿Paralelismo y concurrencia son lo mismo?
¿Cómo sé si mi cuello de botella es CPU o I/O?
¿Cuándo no vale la pena paralelizar?
¿Cuántos workers debería usar?
¿Qué hago si una tarea paralela falla a mitad del proceso?
¿El paralelismo siempre mejora la latencia?
¿Qué stack se beneficia más de estas ideas?
Azirgo
¿Listo para construir tu Producto Digital?
Sitios web, apps móviles, software a medida y soluciones blockchain. Cuéntanos qué tienes en mente y armamos un plan claro contigo.
- Cotización clara en 48 horas
- Equipo en Ecuador, atención en español
- Desde un MVP hasta un producto en producción