Un poco de contexto sobre quién escribe esto y por qué. Somos Geonode y vendemos proxies, lo que nos mantiene constantemente en contacto con este tema: el web scraping es la carga de trabajo por excelencia limitada por la E/S, y «mi scraper va lento» es una de las quejas más habituales que nos plantean los usuarios. Así que aquí va la versión sincera antes de pasar a la teoría: si tu scraper es lento porque recurre una página cada vez, comprar proxies no lo acelerará. La concurrencia es una propiedad de tu código. Cien puntos de conexión de proxy y un bucle secuencial te darán un scraper secuencial con cien puntos de conexión inactivos. Soluciona primero la concurrencia. Los proxies resuelven un problema diferente: el que surge después de que la concurrencia funcione, cuando el sitio de destino empieza a limitar el tráfico de la dirección que, de repente, está realizando cincuenta solicitudes por segundo. Ambos problemas son reales. No son el mismo problema y no tienen la misma solución.
Dicho esto, veamos la distinción.
La diferencia en una sola frase
La formulación de Rob Pike, extraída de su charla sobre el tema, sigue siendo la más clara, y el blog de Go lo expresa directamente:
Concurrencia es «la composición de procesos que se ejecutan de forma independiente».
El paralelismo es «la ejecución simultánea de cálculos (posiblemente relacionados)».
Léelo dos veces, porque la diferencia radica en dónde se pone el énfasis. La concurrencia tiene que ver con la estructura: cómo se descompone un problema en partes que pueden ejecutarse de forma independiente. El paralelismo tiene que ver con la ejecución: cuántas de esas partes se ejecutan físicamente al mismo tiempo.
La consecuencia que la gente pasa por alto: la concurrencia es algo que tú escribes; el paralelismo es algo que hace la máquina. Puedes escribir un programa concurrente y ejecutarlo en un solo núcleo, donde nada es simultáneo en ningún momento, y seguirá siendo concurrente. Has descrito tareas independientes; el entorno de ejecución las intercala. Y un programa concurrente ejecutado en ocho núcleos puede llegar a ser paralelo, si el entorno de ejecución y la carga de trabajo lo permiten.
Esa asimetría es la razón por la que ambos términos no son intercambiables. La concurrencia permite el paralelismo sin garantizarlo. El paralelismo sin una estructura concurrente no es posible en absoluto.
Por qué persiste la confusión
Hay tres razones, y nombrarlas ayuda a entenderlo.
El comportamiento observable suele ser idéntico. Tanto un programa concurrente en un núcleo como un programa paralelo en cuatro núcleos dan la impresión de que «están ocurriendo varias cosas a la vez». Desde fuera, no se puede distinguir cuál de los dos es. Solo te das cuenta cuando añades núcleos y no se nota ninguna mejora.
El vocabulario de cada lenguaje es inconsistente. El módulo threading de Python te ofrece concurrencia, pero históricamente no paralelismo. El módulo multiprocessing de Python te ofrece ambas cosas. Las funciones async y await de JavaScript te ofrecen concurrencia, pero nunca paralelismo para tu propio código. Las goroutines de Go te ofrecen concurrencia y paralelismo hasta GOMAXPROCS. Los mismos términos en la documentación tienen significados diferentes según el ecosistema.
La mayoría de las veces no hace falta que te preocupes, hasta que de repente sí que tienes que hacerlo. Para una carga de trabajo limitada por la E/S, la distinción es casi puramente teórica: la concurrencia por sí sola te da toda la ventaja. En el caso de una carga de trabajo limitada por la CPU, la situación cambia por completo, ya que la concurrencia por sí sola no aporta nada. El problema es que la gente aprende el patrón que funcionó en su problema limitado por la E/S y lo aplica a uno limitado por la CPU.
Cómo lo hace realmente cada lenguaje
| Entorno de ejecución | Mecanismo de concurrencia | Paralelismo real para tu código | Dónde falla |
|---|
| Python (compilación predeterminada) | threading, asyncio | Solo a través de multiprocessing | El GIL serializa la ejecución del código de bytes |
| Python (compilación con subprocesos libres) | threading, asyncio | Sí, los subprocesos se ejecutan en paralelo | Sobrecarga de un solo subproceso, madurez del ecosistema |
| Go | goroutines + canales | Sí, hasta GOMAXPROCS | El estado compartido sigue necesitando sincronización |
| Node.js | bucle de eventos, async/await | Solo a través de worker_threads o procesos hijos | Una llamada de retorno que consume recursos de la CPU lo bloquea todo |
| Java / C# | subprocesos, grupos de subprocesos | Sí | Complejidad del estado mutable compartido |
| Rust | async + subprocesos | Sí | El compilador te obliga a hacerlo bien desde el principio |
Node.js es el ejemplo más claro de concurrencia sin paralelismo. La documentación oficial lo explica con precisión: el bucle de eventos «permite a Node.js realizar operaciones de E/S sin bloqueo —a pesar de que, por defecto, se utiliza un único hilo de JavaScript— al descargar las operaciones al núcleo del sistema siempre que sea posible». El núcleo es multihilo; tu código JavaScript no lo es. El bucle recorre seis fases —temporizadores, callbacks pendientes, inactivo/preparación, sondeo, comprobación, cierre de callbacks— y, cuando finaliza una operación, «el núcleo se lo comunica a Node.js para que el callback correspondiente pueda añadirse a la cola de sondeo y, finalmente, ejecutarse».
La consecuencia práctica se deduce directamente. Diez mil solicitudes HTTP simultáneas en Node son algo trivial, porque la espera tiene lugar en el núcleo. Una función que consume muchos recursos de la CPU y se ejecuta durante dos segundos bloquea todo el proceso, ya que solo hay un hilo en el que ejecutarla y nada puede tener prioridad sobre ella.
Go adopta el enfoque contrario: las goroutines son lo suficientemente baratas como para crearlas por miles, y el programador las distribuye entre los hilos del sistema operativo hasta un límite de «GOMAXPROCS», cuyo valor por defecto es el número de núcleos disponibles. Así pues, Go te ofrece concurrencia y paralelismo a partir de la misma construcción. Por eso Pike impartió la charla: la distinción cobra mayor importancia en un lenguaje en el que se obtienen ambas cosas y, por lo tanto, se pueden confundir.
La pregunta decisiva: ¿tu trabajo está limitado por la E/S o por la CPU?
Todo lo anterior se reduce a una pregunta sobre tu carga de trabajo, y merece la pena medirla en lugar de darla por sentada.
Limitado por la E/S significa que tu programa pasa la mayor parte del tiempo esperando: una respuesta de red, una lectura de disco o una consulta a la base de datos. Durante la espera, la CPU permanece inactiva. La concurrencia es la respuesta correcta y suficiente, ya que permite iniciar la siguiente espera mientras la actual sigue pendiente. El paralelismo no aporta prácticamente nada: ocho núcleos esperando a la red no son más rápidos que un solo núcleo esperando a la red.
Limitado por la CPU significa que tu programa dedica la mayor parte del tiempo a realizar cálculos: análisis sintáctico, compresión, hash, transformación. La CPU está saturada. La concurrencia por sí sola no cambia nada: intercalar dos cálculos en un núcleo lleva el mismo tiempo total que ejecutarlos en secuencia, más la sobrecarga de conmutación. Solo el paralelismo ayuda, y solo hasta el número de núcleos físicos.
Para averiguar cuál es tu caso, mide en lugar de razonar. En Linux, time te da la respuesta de inmediato: compara el tiempo real transcurrido con el tiempo de CPU de usuario más el de sistema. Si el tiempo real es mucho mayor que el tiempo de CPU, estás esperando: estás limitado por la E/S. Si están próximos, estás procesando: estás limitado por la CPU.
El scraping web es un ejemplo útil porque presenta ambas situaciones, de forma secuencial. La recuperación de páginas está muy limitada por la E/S; el análisis posterior del HTML está limitado por la CPU. La arquitectura adecuada utiliza la concurrencia para la recuperación y el paralelismo para el análisis, y el error habitual es aplicar una misma estrategia a ambas etapas. Un rastreador con 200 recuperaciones simultáneas que alimentan a un analizador de un solo hilo no es un rastreador rápido: es un recuperador rápido con una cola de trabajo acumulándose detrás.
El GIL de Python y lo que ha cambiado con el «free threading»
Python merece una sección propia porque su situación ha cambiado de verdad y gran parte de lo que leerás al respecto ya está desactualizado.
Históricamente: el bloqueo global del intérprete (GIL) de CPython solo permitía que un hilo ejecutara el bytecode de Python a la vez. Por lo tanto, los hilos proporcionaban concurrencia, pero no paralelismo. El GIL se libera durante las operaciones de E/S, por lo que la E/S con hilos funcionaba bien; sin embargo, el uso de hilos con carga de CPU no funcionaba, y la solución era utilizar «multiprocessing».
Qué ha cambiado: a partir de la versión 3.13, CPython incluye una compilación opcional con el GIL desactivado. La documentación sobre subprocesos libres lo describe claramente: «La ejecución con subprocesos libres permite aprovechar al máximo la potencia de procesamiento disponible ejecutando subprocesos en paralelo en los núcleos de CPU disponibles».
PEP 779, aceptada por el Consejo Directivo el 16 de junio de 2025 con estado «Final», estableció los criterios para pasar el «free-threading» de la fase experimental a la de soporte oficial, fijando Python 3.14 como objetivo para esa fase.
Cuatro aspectos prácticos que debes tener en cuenta antes de utilizarlo:
No es la compilación predeterminada. Tienes que obtenerla o compilarla expresamente —a partir del código fuente, lo que implica la opción de configuración «--disable-gil». Comprueba qué versión estás ejecutando con python -VV, que muestra «free-threading build», o con sys._is_gil_enabled(), que devuelve «False» cuando el GIL está desactivado.
El código de un solo hilo se vuelve más lento. La documentación indica que, en la suite pyperformance, «la sobrecarga media oscila entre aproximadamente el 1 % en macOS aarch64 y el 8 % en sistemas Linux x86-64». La PEP 779 recoge que el Consejo Directivo espera que Python con subprocesos libres «sea entre un 10 % y un 15 % más lento», con un 15 % como objetivo firme para la fase II, y acepta un aumento de la media geométrica del 20 % en el uso de memoria como «el coste de disponer de subprocesos libres eficientes y seguros». Si tu programa es de un solo hilo, esta compilación supone un claro retroceso.
Puedes volver a activar el GIL en tiempo de ejecución. Las compilaciones con subprocesos libres admiten la ejecución con el GIL activado mediante la variable de entorno PYTHON_GIL o la opción -X gil, lo cual resulta útil cuando una dependencia no funciona correctamente.
Tus dependencias son la limitación. Las extensiones C deben compilarse para declarar la compatibilidad con el «free-threading». El ecosistema ha evolucionado considerablemente, pero «funciona en mi máquina con Python puro» no es lo mismo que «mi pila científica funciona».
En el caso de las operaciones limitadas por E/S, que predominan en el scraping y el trabajo con API, nada de esto cambia tu decisión: asyncio o un grupo de subprocesos en la compilación estándar ya te ofrece todo lo que la concurrencia puede aportar. El «free threading» es importante cuando el cuello de botella se encuentra en la fase de análisis, y no en la de obtención de datos.
Un ejemplo práctico: recuperar 10 000 URL
Las cifras concretas hacen que la diferencia resulte evidente. Supongamos que cada solicitud tarda 200 ms y que el análisis de cada respuesta consume 50 ms de CPU, en un equipo de cuatro núcleos.
Secuencial. 10 000 × 250 ms = 2 500 segundos, unos 42 minutos. La CPU permanece inactiva el 80 % de ese tiempo.
Recuperación concurrente, análisis secuencial. Con 100 solicitudes concurrentes, la recuperación se reduce a unos 20 segundos de tiempo real. El análisis no varía: 10 000 × 50 ms = 500 segundos. Un total de unos 520 segundos, aproximadamente 9 minutos. Una mejora de 4,8 veces —y fíjate en dónde se ha ido el tiempo—. La recuperación suponía el 80 % del tiempo de ejecución original y ahora es el 4 % del nuevo. El análisis, que no has modificado, supone ahora el 96 % del total.
Recuperación concurrente y análisis paralelo en cuatro núcleos. El análisis se reduce a unos 125 segundos. En total, unos 145 segundos, aproximadamente 2,5 minutos. Una mejora de 17 veces respecto a la ejecución secuencial.
De estas cifras se desprenden tres lecciones.
En primer lugar, la mayor mejora se consigue al resolver el cuello de botella de E/S mediante la concurrencia, y es prácticamente gratuita: sin núcleos adicionales, sin problemas de estado compartido, solo un bucle diferente.
En segundo lugar, una vez que se resuelve el cuello de botella dominante, el siguiente pasa a ser inmediatamente el más importante. Esta es la ley de Amdahl en su forma más práctica: optimizar una etapa que supone el 20 % del tiempo de ejecución no puede suponer una aceleración superior al 25 %, por muy completamente que se elimine. Mide siempre antes de optimizar y vuelve a medir después, porque la respuesta cambia.
En tercer lugar —y aquí es donde nuestro interés comercial cobra relevancia, así que valóralo en consecuencia—: en el momento en que pasas de una solicitud a la vez a cien, te haces visible. Una sola dirección que realice 500 solicitudes por segundo a un host se verá limitada en su tasa de solicitudes y, posteriormente, bloqueada. Eso no es un problema de concurrencia y ninguna cantidad de «asyncio» lo soluciona; es un problema de distribución, y para eso sirven los proxies. Nuestro tráfico residencial cuesta a partir de 0,79 $/GB y el de centro de datos, a partir de 0,14 $/GB, según nuestra página de precios en septiembre de 2026. Pero fíjate en el orden: primero la concurrencia y, después, los proxies cuando la concurrencia genere un problema que no pueda resolver. Hacerlo al revés significa pagar por un ancho de banda que no tienes forma de utilizar.
Cuando la concurrencia deja de ser útil
Aumentar la concurrencia tiene un rendimiento decreciente y, posteriormente, negativo, y el punto de inflexión llega antes de lo que la mayoría de la gente espera.
Límites de conexión. Los sistemas operativos limitan el número de descriptores de archivo abiertos. Los servidores limitan el número de conexiones simultáneas por cliente. Diez mil solicitudes simultáneas procedentes de una misma máquina alcanzarán uno de estos límites mucho antes de llegar al límite de la CPU, y el modo de fallo suele ser un error confuso en lugar de uno claro.
Memoria. Cada solicitud en curso contiene búferes, encabezados analizados y datos de respuesta pendientes. Diez mil solicitudes simultáneas, cada una de las cuales ocupa 100 KB, suponen un gigabyte de memoria que no hace más que esperar.
Sobrecarga por cambio de contexto. Los subprocesos del sistema operativo no son gratuitos: cada uno conlleva una pila y un coste de programación. Esta es precisamente la razón por la que existen las goroutines y las coroutines: son lo suficientemente baratas como para que sea razonable utilizar miles de ellas, mientras que no lo es con miles de subprocesos del sistema operativo.
La tolerancia del destino. El otro extremo de la conexión tiene sus propias limitaciones. A partir de cierta tasa, la concurrencia adicional produce códigos de error 429 y 503 en lugar de datos, y tu rendimiento efectivo disminuye a medida que añades más. Este es el límite máximo más habitual en el mundo real y el que menos se mide, porque las solicitudes siguen «funcionando»: simplemente devuelven errores que un bucle de reintentos repite diligentemente.
El enfoque práctico carece de glamour: empieza con un límite de concurrencia modesto, mide las solicitudes completadas por segundo en lugar de las solicitudes intentadas, y ve aumentando hasta que el rendimiento deje de mejorar. Se estabilizará y, a continuación, descenderá. El punto óptimo se encuentra en esa meseta, y suele ser una cifra mucho menor de lo que sugiere la intuición —a menudo decenas en lugar de cientos—.
Cuando no necesitas ninguna de las dos cosas
Vale la pena mencionarlo, porque «hacerlo concurrente» se ha convertido en un acto reflejo.
Cuando la tarea es realmente pequeña. Cien peticiones que tardan 200 ms cada una suman 20 segundos en ejecución secuencial. Si se ejecuta cada noche mediante una tarea cron, 20 segundos están bien, y el código concurrente es más difícil de depurar cuando falla a las 3 de la madrugada.
Cuando el orden forma parte de los requisitos. Algunas secuencias de procesamiento deben procesar los elementos en un orden estricto, o cada paso depende del resultado anterior. En este caso, la concurrencia no solo no ayuda, sino que es una fuente de errores que solo aparecen bajo carga.
Cuando el cuello de botella está en otro sitio por completo. Si las escrituras en la base de datos son la limitación, 200 lectores concurrentes solo crean una cola más larga delante del mismo bloqueo. Soluciona el cuello de botella real. La concurrencia aguas arriba de un recurso en serie convierte un programa lento en un programa lento con un problema de memoria.
Cuando el estado compartido es complicado. El código concurrente que afecta al estado mutable compartido necesita sincronización, y si se hace mal se produce el peor tipo de error: intermitente, dependiente de la carga e irreproducible en tu máquina. Si la aceleración es del doble y el estado es complejo, el código secuencial, sobre el que puedes razonar, suele ser la mejor decisión de ingeniería.
Y la versión que nos concierne: si estás rastreando unos cientos de páginas al día de un sitio web al que no le importa, no necesitas ni concurrencia ni proxies. Una llamada a requests en un bucle con un retraso cortés es la respuesta correcta, y preferimos decírtelo antes que venderte un plan que no necesitas.
Preguntas frecuentes
¿Cuál es la diferencia más sencilla entre concurrencia y paralelismo?
La concurrencia consiste en gestionar muchas cosas a la vez: es una propiedad estructural de cómo se escribe el programa. El paralelismo consiste en hacer muchas cosas a la vez: es una propiedad física de cómo se ejecuta. La concurrencia hace posible el paralelismo; no es lo que lo hace que ocurra.
¿Puede haber paralelismo sin concurrencia?
No de forma útil en el sentido que se trata aquí. La ejecución paralela requiere unidades de trabajo independientes que se puedan distribuir, y definir esas unidades es precisamente lo que significa la concurrencia. El paralelismo a nivel de hardware, como el SIMD, es una excepción: paraleliza un único flujo de instrucciones sobre los datos sin que haya ninguna estructura concurrente en el programa.
¿Sigue existiendo el GIL de Python?
Sí, en la compilación predeterminada. Desde la versión 3.13, CPython también incluye una compilación opcional de subprocesos libres con el GIL desactivado, y la PEP 779 ha elevado esa compilación a la categoría de soporte oficial de cara a la versión 3.14. La compilación predeterminada sigue contándolo, por lo que, a menos que hayas instalado deliberadamente un intérprete de subprocesos libres, el GIL está presente.
¿Es «async» lo mismo que el multihilo?
No. La concurrencia asíncrona utiliza un único hilo con conmutación cooperativa en puntos «await» explícitos, por lo que solo se ejecuta una parte de tu código a la vez y las conmutaciones solo se producen donde las has escrito. El multihilo utiliza varios hilos del sistema operativo con conmutación preemptiva que puede producirse en cualquier lugar. La asincronía es más fácil de comprender; los hilos pueden alcanzar un paralelismo real cuando el tiempo de ejecución lo permite.
¿Cuántas solicitudes simultáneas debería realizar?
Menos de las que crees. Empieza con unas 10, mide las solicitudes completadas por segundo y ve aumentando hasta que esa cifra deje de crecer. El límite máximo suele ser la tolerancia del servidor de destino, más que la capacidad de tu máquina, y, más allá de ese punto, una mayor concurrencia genera errores en lugar de aumentar el rendimiento.
¿La concurrencia hace que mi código sea más rápido?
Solo si estás esperando algo. Para tareas limitadas por la E/S, las mejoras son considerables. Para tareas limitadas por la CPU en un único núcleo, la concurrencia ralentiza ligeramente el proceso debido a la sobrecarga de la conmutación: necesitas paralelismo, lo que implica múltiples núcleos y un entorno de ejecución capaz de utilizarlos.
¿Cuál es la diferencia entre multiprocesamiento y multihilo?
Los hilos comparten memoria dentro de un mismo proceso, lo que hace que la comunicación sea sencilla y que el estado compartido resulte peligroso. Los procesos tienen memoria independiente, lo que los hace seguros, aunque la comunicación resulta costosa. En la versión predeterminada de Python, los procesos son la forma de conseguir un paralelismo real para tareas limitadas por la CPU; los subprocesos te proporcionan concurrencia para tareas limitadas por la E/S.
¿Necesito proxies para ejecutar solicitudes concurrentes?
No necesariamente. Los necesitas cuando la concurrencia te hace lo suficientemente visible como para que un destino limite la tasa o bloquee la dirección desde la que te conectas. Frente a una API con una cuota generosa, o un sitio web que tienes permiso para rastrear, la concurrencia por sí sola es suficiente. Frente a un sitio web que impone límites por dirección, la distribución se convierte en la limitación —y eso es algo que hay que solucionar aparte de arreglar tu código.
Conclusión
Merece la pena tener presente esta distinción, ya que convierte una pregunta vaga —«¿cómo puedo acelerar esto?»— en una pregunta concreta con una respuesta comprobable: ¿estoy esperando o estoy procesando?
Si estás esperando, necesitas concurrencia, y la necesitas en cualquier forma que ofrezca tu lenguaje. La mejora es considerable, normalmente no supone ningún coste adicional en hardware y está disponible en todos los entornos de ejecución habituales. Si estás procesando datos, la concurrencia por sí sola no te servirá de nada y necesitarás un paralelismo auténtico: procesos, subprocesos de trabajo, goroutines repartidas entre núcleos o, en el caso de Python, posiblemente un intérprete de subprocesos libres con sus propias ventajas e inconvenientes que sopesar.
La mayoría de los programas reales son ambas cosas, en distintas fases, y el orden importa más que la elección. Soluciona el cuello de botella dominante, vuelve a medir y espera que la respuesta haya cambiado. Una cadena de procesamiento que estaba limitada en un 80 % por la red pasa a estarlo en un 96 % por el análisis sintáctico en el momento en que solucionas el problema de la red, y la segunda optimización es un trabajo completamente diferente al primero.