Matías Fernández / System Design PYTHON · FIELD NOTE 01

System Design Field Notes Python

PYTHON · EJECUCIÓN · DISEÑO DE SISTEMAS

Concurrencia
vs. paralelismo.

La diferencia importante no es cuántos threads aparecen en un diagrama. Es si el sistema necesita administrar mejor la espera o hacer más cálculo al mismo tiempo. Esa respuesta cambia el modelo de ejecución, los límites, los fallos y hasta las métricas que importan.

EL MISMO TRABAJODOS FORMAS DE AVANZAR
CONCURRENCIA1 worker · tareas intercaladas
ABACBC
t₀tiempo →
PARALELISMO3 workers · ejecución simultánea
ABC
t₀tiempo →

solapamiento simultaneidad

PUBLICADO
10 AGO 2026
LECTURA
~16 min
ENFOQUE
Python 3.11+
OBJETIVO
Decidir antes de escalar

01 / EL MODELO MENTAL

Dos problemas distintos.
Dos objetivos distintos.

Concurrencia y paralelismo pueden coexistir, pero no son sinónimos. Separarlos evita optimizar el recurso equivocado y ayuda a explicar qué garantía compra cada pieza de arquitectura.

CONCURRENCIA

Coordinar muchas tareas que están en progreso.

Una tarea avanza mientras otra espera. Pueden intercalarse en un solo core o correr sobre varios. El objetivo principal es capacidad de respuesta, throughput y buen uso de recursos que de otro modo quedarían ociosos.

PREGUNTA¿Cómo atendemos muchas cosas sin bloquearnos?
PARALELISMO

Ejecutar trabajo realmente al mismo tiempo.

El problema se divide entre cores, procesos, intérpretes o máquinas. El objetivo es aumentar capacidad de cómputo o reducir la duración de una carga CPU-bound que admite partición.

PREGUNTA¿Cómo hacemos más cálculo simultáneo?

Pensalo con una API. Mientras espera una consulta a PostgreSQL, puede aceptar otra conexión: eso es concurrencia. Si luego debe comprimir cuatro imágenes usando cuatro cores a la vez, eso es paralelismo. El primer modelo aprovecha la espera; el segundo agrega capacidad de ejecución.

Ninguno garantiza por sí solo que el sistema sea más rápido. Cada tarea concurrente agrega estado vivo; cada worker paralelo agrega coordinación y movimiento de datos. A partir de cierto punto, más unidades de ejecución solo producen colas, context switching y contención.

UNA DISTINCIÓN ÚTIL

Concurrencia es una propiedad del diseño. Paralelismo es una propiedad de la ejecución.

02 / DIAGNOSTICAR ANTES DE ELEGIR

No empieces por
async vs. threads.

Empezá por el perfil de la carga. Una decisión de ejecución sin mediciones es una hipótesis disfrazada de arquitectura.

  1. 01

    ¿Dónde se va el tiempo?

    Separá espera externa, cómputo, memoria, serialización, locks y tiempo en cola. Usá perfiles y trazas, no intuición.

  2. 02

    ¿Qué resultado querés mover?

    Latencia por request, throughput total, capacidad de respuesta, costo por tarea y tiempo de recuperación pueden pedir soluciones opuestas.

  3. 03

    ¿Cuánto cuesta coordinar?

    Medí tamaño de los mensajes, frecuencia de sincronización, memoria por worker y granularidad. El trabajo debe amortizar ese costo.

  4. 04

    ¿Qué pasa cuando se satura?

    Definí límites, prioridad, timeout, cancelación, degradación y rechazo. La capacidad finita necesita una conducta finita.

SEÑAL OBSERVADAOBJETIVOMODELOPUNTO DE PARTIDA EN PYTHON
La mayor parte del tiempo se va esperando red, disco o base de datosAtender más trabajo sin dejar recursos ociososConcurrenciaasyncio; threads si la librería es bloqueante
La CPU permanece alta ejecutando Python puroReducir el tiempo total del cálculoParalelismoProcessPoolExecutor; intérpretes en Python 3.14+
Hay esperas y etapas intensivas de cómputoAislar cada cuello de botella y controlar el flujoHíbridoasyncio en el borde + cola + workers de CPU
La tarea es pequeña o necesita compartir estado constantementeEvitar que la coordinación cueste más que el trabajoSecuencial primeroMedir antes de agregar executors o coroutines

03 / EL TOOLBOX DE PYTHON

Elegí por el cuello de botella,
no por familiaridad.

Python ofrece varias primitivas. La mejor es la que hace explícito el modelo de ejecución y contiene su costo operativo.

01

asyncio

USALO PARA
Muchas operaciones de I/O con librerías async.
COMPRA
Miles de tareas livianas, cancelación y coordinación explícitas.
COSTO
Una llamada bloqueante detiene el event loop completo.
02

Threads

USALO PARA
I/O bloqueante, SDKs sin API async y compatibilidad con código existente.
COMPRA
Memoria compartida y una migración incremental sencilla.
COSTO
Carreras, locks y, en CPython tradicional, sin speedup para CPU Python puro.
03

Procesos

USALO PARA
Cómputo Python puro que puede dividirse en unidades independientes.
COMPRA
Paralelismo multinúcleo y aislamiento de memoria.
COSTO
Serialización, memoria adicional, startup y comunicación entre procesos.
04

Intérpretes

USALO PARA
CPU en Python 3.14+ cuando el aislamiento por intérprete encaja.
COMPRA
Paralelismo real dentro de un proceso, con un GIL por intérprete.
COSTO
Estado mutable aislado y datos serializables entre workers.

CONCURRENCIA ACOTADA

`async` no significa
“sin límites”.

Este ejemplo mantiene como máximo veinte requests en vuelo, aplica un timeout por operación y agrupa el ciclo de vida de las tareas. Si una tarea falla con un error no manejado, TaskGroup cancela sus hermanas: esa es una decisión de semántica, no un detalle de sintaxis.

  • Semaphore protege la dependencia.
  • asyncio.timeout limita la espera.
  • TaskGroup evita tareas huérfanas.
bounded_fetch.pyPYTHON 3.11+
import asyncio
import httpx

async def fetch_one(client, url, slots):
    async with slots:
        try:
            async with asyncio.timeout(2.0):
                response = await client.get(url)
                response.raise_for_status()
                return response.json()
        except TimeoutError:
            return {"url": url, "error": "timeout"}

async def fetch_many(urls):
    slots = asyncio.Semaphore(20)
    async with httpx.AsyncClient() as client:
        async with asyncio.TaskGroup() as group:
            tasks = [
                group.create_task(fetch_one(client, url, slots))
                for url in urls
            ]
    return [task.result() for task in tasks]

Si un SDK bloqueante no ofrece una API async, await asyncio.to_thread(...) permite sacarlo del event loop. Es una herramienta de compatibilidad muy útil para I/O; en la build tradicional de CPython no transforma automáticamente código Python CPU-bound en trabajo multinúcleo.

Para cómputo independiente, un ProcessPoolExecutor evita el GIL usando procesos separados. La función, argumentos y resultados deben poder serializarse, y el bloque debe ser lo bastante grande para amortizar ese viaje.

parallel_transform.pyCPU-BOUND
from concurrent.futures import ProcessPoolExecutor

def transform(chunk):
    # Trabajo CPU-bound, sin estado compartido.
    return expensive_normalization(chunk)

def transform_all(chunks):
    with ProcessPoolExecutor() as pool:
        return list(pool.map(transform, chunks, chunksize=8))

if __name__ == "__main__":
    result = transform_all(load_chunks())

04 / DEL RUNTIME AL SISTEMA

El patrón que escala
suele ser híbrido.

Una aplicación real mezcla conexiones, cómputo y dependencias lentas. El objetivo no es elegir un ganador: es darle a cada etapa un modelo y un presupuesto propios.

CASO: PIPELINE DE DOCUMENTOSCONCURRENCIA EN EL BORDE · PARALELISMO EN CPU
01Clientesrequests concurrentes
02API asyncvalidar + persistir
03Cola acotadabackpressure
04 · WORKERS CPU
OCR AOCR BOCR C
05Resultadoestado idempotente
LÍMITESTIMEOUTSRETRIES + JITTEROBSERVABILIDADCANCELACIÓN

La API no procesa OCR dentro del request. Acepta trabajo, lo hace durable y devuelve un identificador. La cola desacopla ritmos; los workers paralelos consumen según la CPU disponible; el cliente observa progreso sin mantener recursos pesados abiertos.

01

Bounded concurrency

Si la base admite 40 conexiones, lanzar 400 queries no crea capacidad. Crea una cola dentro del lugar menos observable.

02

Backpressure

Cuando el consumidor se atrasa, el productor debe esperar, degradar o rechazar. Una cola infinita solo aplaza el incidente.

03

Bulkheads

Separá pools por tipo de trabajo o prioridad. Un reporte pesado no debería consumir la capacidad del login.

04

Idempotencia

Timeout no significa fracaso: significa resultado desconocido. Reintentar solo es seguro si repetir no duplica el efecto.

05 / CÓMO FALLA EN PRODUCCIÓN

Más tareas también son
más estados posibles.

La complejidad real aparece en saturación, cancelaciones y resultados parciales. El modelo está incompleto hasta que esos estados tienen una respuesta explícita.

01

Fan-out sin límite

FALLA

Crear una tarea por elemento parece eficiente hasta que 50.000 requests agotan sockets, memoria o el pool de la base de datos.

DISEÑO

Límite explícito, cola acotada y rechazo o degradación cuando no hay capacidad.

02

CPU dentro del event loop

FALLA

Una transformación pesada bloquea todas las conexiones aunque la ruta esté escrita con async def.

DISEÑO

Mover la etapa a procesos, intérpretes o una librería nativa que libere el GIL.

03

Retries sincronizados

FALLA

Cuando una dependencia vuelve lentamente, todos los workers reintentan juntos y provocan una segunda caída.

DISEÑO

Backoff exponencial con jitter, presupuesto de reintentos e idempotencia.

04

Estado mutable compartido

FALLA

El resultado depende del orden exacto de ejecución y aparecen carreras difíciles de reproducir.

DISEÑO

Mensajes inmutables, ownership claro, locks pequeños o partición por clave.

LAS PREGUNTAS QUE EL CÓDIGO NO RESPONDE SOLO

  • TIMEOUT

    ¿El usuario puede reintentar o el trabajo continúa en background?

  • CANCELACIÓN

    ¿Se propaga a subtareas y workers o solo dejamos de esperar?

  • PARCIAL

    ¿Devolvemos lo disponible, fallamos todo o marcamos cada elemento?

  • ORDEN

    ¿Importa globalmente, por cliente, por documento o no importa?

06 / UN MÉTODO REPETIBLE

Diseñar desde la carga
hasta la operación.

Este recorrido sirve para una API, un crawler, un pipeline de datos o un servicio de procesamiento multimedia.

  1. 01

    Medí la versión simple

    Perfilá CPU, I/O, memoria y locks. Registrá p50, p95 y p99, no solo duración media.

  2. 02

    Nombrá la unidad de trabajo

    Request, archivo, partición, imagen o cliente. Definí qué estado comparte y qué orden necesita.

  3. 03

    Elegí el modelo por etapa

    Async para esperar sin bloquear; threads para I/O bloqueante; procesos o intérpretes para CPU aislable.

  4. 04

    Poné límites donde nace el trabajo

    Semáforos, pools y colas acotadas. Si no hay capacidad, la respuesta debe ser visible y deliberada.

  5. 05

    Diseñá el ciclo de vida

    Timeout, cancelación, retry, idempotencia, shutdown y recuperación de tareas huérfanas.

  6. 06

    Probá el sistema, no el happy path

    Carga sostenida, bursts, dependencia lenta, worker muerto y datos con skew. Observá dónde aparece la primera cola.

DESIGN REVIEW

Antes de aprobar
el diagrama.

  • 01Puedo decir si el cuello de botella es espera, CPU, memoria o contención.
  • 02Definí si optimizo latencia, throughput, costo o capacidad de respuesta.
  • 03Cada recurso finito tiene un límite: conexiones, tareas, cola y workers.
  • 04Timeout, cancelación y resultado parcial tienen una semántica de producto.
  • 05Los retries son acotados, espaciados e idempotentes.
  • 06El orden requerido está definido por entidad; no impongo orden global sin necesidad.
  • 07Mido tiempo en cola, tiempo de servicio, tareas activas, saturación y errores.
  • 08Probé con carga y fallos; no inferí capacidad desde un ejemplo local.

LA DECISIÓN EN UNA LÍNEA

Espera mejor con concurrencia.
Calculá más con paralelismo.

Y cuando el sistema haga ambas cosas, separá las etapas, acotá el trabajo en vuelo y tratá saturación, cancelación y retries como comportamiento de producto. La arquitectura correcta no es la que ejecuta más tareas: es la que mantiene control cuando llega el pico.

07 / FUENTES Y PRÓXIMOS PASOS

Para seguir
profundizando.

  1. 01Concurrency vs. ParallelismEl artículo que disparó esta nota · freeCodeCamp ↗
  2. 02Coroutines and TasksTaskGroup, cancelación y timeouts · Python Docs ↗
  3. 03concurrent.futuresThreads, procesos e intérpretes · Python Docs ↗
  4. 04Python support for free threadingBuilds sin GIL y limitaciones actuales · Python Docs ↗