Benchmark de latencia secuencial con y sin llamada al modelo, ejecutado sobre Microsoft Foundry.

Una frase antes de los números

Dos agentes desplegados en el mismo proyecto de Microsoft Foundry, en la misma región, con la misma cuota, la misma configuración de Key Vault y el mismo sobre de recursos del contenedor. Uno está implementado con C# 14 sobre .NET 10 LTS; el otro, con Python 3.12 sobre CPython.

El resultado principal es fácil de resumir: en esta serie de ejecuciones, la implementación C#/.NET obtuvo entre un 22 % y un 30 % menos de latencia media que la implementación Python, y la diferencia apareció tanto con llamada al modelo como sin ella.

Ese es el dato medido. Lo que no he medido todavía es igualmente importante: este experimento no determina el throughput máximo, el comportamiento bajo saturación ni el número real de instancias que necesitaría cada implementación en producción. Las pruebas se ejecutaron con una sola petición en vuelo y a ritmo bajo. Más adelante utilizaré los resultados para construir un modelo de capacidad y coste, pero lo presentaré como lo que es: una proyección con supuestos, no una factura de Azure.

Ahora sí: qué comparé, cómo lo medí y qué conclusiones permite sacar.

Qué comparo exactamente

Son dos Hosted Agents funcionalmente equivalentes desplegados uno al lado del otro:

Aspecto ippoc-agentcsharp ippoc-agentpython
Lenguaje C# 14 Python 3.12
Runtime .NET 10 LTS (aspnet:10.0) CPython 3.12 (python:3.12-slim)
SDK de servidor Azure.AI.AgentServer.Responses 1.0.0-beta.3 azure-ai-agentserver-responses 1.0.0b8
Handler ProtectedResponseHandler.cs protected_response_handler.py

La infraestructura se mantiene constante, pero conviene ser preciso con la variable experimental: no estoy comparando dos intérpretes ejecutando el mismo binario. Estoy comparando dos stacks de implementación completos, C#/.NET y Python/CPython, incluidos sus SDK, su servidor y su ruta de serialización.

Por tanto, el resultado no permite atribuir cada milisegundo exclusivamente al runtime. Sí permite responder a una pregunta arquitectónica más útil: si implemento este mismo agente en cada uno de los dos stacks soportados, ¿qué comportamiento observo en el Hosted Agent real?

Por qué Python 3.12 y no 3.14

A julio de 2026, Python 3.14 es la serie estable más reciente y Python 3.15 continúa en pre-release. He utilizado Python 3.12 deliberadamente porque es la línea de producción de la PoC y sigue siendo una referencia habitual en sistemas empresariales ya desplegados.

El objetivo no era enfrentar .NET con el intérprete Python más nuevo disponible, sino medir dos alternativas de despliegue realistas para este proyecto. Por eso el título indica la versión exacta y no intenta convertir el resultado en una comparación universal entre C# y Python.

Si tu plataforma utiliza Python 3.13 o 3.14, no deberías trasladar directamente estos porcentajes. La metodología es reproducible; el resultado debe volver a medirse con tu versión, tus dependencias y tu carga.

Referencias de ciclo de vida:

Dos fases para separar el camino del modelo

No todo el tiempo de una petición pertenece al código del agente. En el camino completo también interviene la invocación al modelo. Para observar ambos perfiles utilicé dos modos:

Fase Qué ejecuta Camino de respuesta
Phase A Sin llamada al modelo Guardrails, opacidad y cuerpo determinista con model_call=false
Phase B Con llamada real al modelo Guardrails, opacidad y gpt-5-mini con reasoning_effort=minimal

Phase A mide el camino del Hosted Agent sin inferencia. Phase B mide el recorrido completo para una respuesta corta y barata. No se compara la calidad del modelo.

Las dos fases se desplegaron y ejecutaron separadamente. Por eso, restar sus medias proporciona una estimación del coste añadido por la llamada al modelo, pero no una medición pareada y aislada del modelo.

Metodología

  • Endpoint: protocolo Responses de Foundry, POST /responses.
  • Petición: { "input": "benchmark ping: what is the capital of France?", "store": false, "stream": false }.
  • Autenticación: token de Azure AD para https://ai.azure.com, cacheado y renovado por scripts/benchmark.ps1.
  • Muestras: N=10, N=100 y N=1000 para ambos agentes en Phase A y Phase B.
  • Concurrencia: Parallel=1.
  • Ritmo configurado: TargetRps=0.15, intercalando ambos agentes.
  • Warm-up explícito: Warmup=0.
  • Reintentos: MaxRetries=5; los 401/403 renuevan el token una vez y los 429/503 usan backoff acotado.
  • Artefactos: únicamente resultados frescos bajo out/bench/run-*/.

El interleaving reduce el sesgo temporal frente a ejecutar primero todo C# y después todo Python: los dos agentes atraviesan condiciones de plataforma más próximas. No elimina por completo el jitter, los vecinos ruidosos o los cambios de servicio, pero distribuye mejor esos efectos.

También hay dos límites que deben quedar claros:

  1. Solo existe una ejecución por combinación de fase y tamaño. N=1000 aporta una muestra amplia dentro de esa ejecución, pero no sustituye repeticiones en horas y días diferentes.
  2. Con Parallel=1 y un ritmo bajo, este es un benchmark de latencia secuencial en baja carga, no una prueba de estrés o saturación.

Resultados

Phase B: con gpt-5-mini

Métrica C# N=10 Python N=10 C# N=100 Python N=100 C# N=1000 Python N=1000
Success rate 100 % 100 % 99 % 99 % 99,9 % 99,9 %
Latencia media (ms) 3486 4669 3628 4666 3549 4665
p50 (ms) 3389 4639 3539 4597 3512 4585
p95 (ms) 3887 5078 3995 5367 3933 5265
p99 (ms) 3997 5197 4435 5748 4263 5620
Máximo (ms) 4025 5226 10304 6854 10385 13721

Ejecuciones originales:

  • N=10: out/bench/run-20260718-100915/summary.md
  • N=100: out/bench/run-20260718-101150/summary.md
  • N=1000: out/bench/run-20260718-103717/summary.md

Phase A: sin llamada al modelo

Métrica C# N=10 Python N=10 C# N=100 Python N=100 C# N=1000 Python N=1000
Success rate 100 % 100 % 100 % 100 % 99,9 % 100 %
Latencia media (ms) 3335 4770 3422 4578 3377 4560
p50 (ms) 3344 4691 3316 4518 3353 4506
p95 (ms) 3558 5452 3767 5141 3748 5214
p99 (ms) 3593 5506 4642 5404 4084 5600
Máximo (ms) 3602 5519 10094 5583 11452 11726

Ejecuciones originales:

  • N=10: out/bench/run-20260718-144815/summary.md
  • N=100: out/bench/run-20260718-145047/summary.md
  • N=1000: out/bench/run-20260718-151536/summary.md

La diferencia observada

La comparación de medias queda así:

Fase N Diferencia media Menor latencia de C#
Phase B 10 1183 ms 25,3 %
Phase B 100 1038 ms 22,2 %
Phase B 1000 1116 ms 23,9 %
Phase A 10 1434 ms 30,1 %
Phase A 100 1156 ms 25,3 %
Phase A 1000 1183 ms 26,0 %

La señal es consistente dentro de esta serie: C# mantiene una ventaja aproximada de entre 1 y 1,4 segundos en las seis combinaciones. También presenta valores inferiores en p50, p95 y p99 absolutos.

Eso permite afirmar que, para estas implementaciones, versiones y condiciones, el stack C#/.NET respondió antes. No permite afirmar que C# sea siempre ese porcentaje más rápido que Python en cualquier Hosted Agent, ni que la diferencia se mantenga intacta al aumentar la concurrencia.

Qué aporta la llamada al modelo

En N=1000, la diferencia entre las medias de Phase A y Phase B es:

  • C#: aproximadamente 172 ms.
  • Python: aproximadamente 104 ms.

En esta petición concreta, el modelo utiliza reasoning_effort=minimal y devuelve una respuesta muy corta. Por eso la parte de inferencia representa una fracción pequeña del recorrido completo observado.

La lectura arquitectónica es interesante: cuando el razonamiento es barato, los componentes que rodean al modelo pueden representar buena parte de la latencia total. Sin embargo, estas cifras no deben presentarse como la latencia aislada de gpt-5-mini: Phase A y Phase B se ejecutaron en despliegues y momentos distintos, y la plataforma introduce variabilidad propia.

Con generaciones de 8, 15 o 30 segundos, la diferencia entre runtimes probablemente tendría mucho menos peso porcentual. Este benchmark describe un camino corto, no todos los perfiles de agente.

Sobre los percentiles y los outliers

Las medianas y los percentiles centrales son estables entre tamaños. Los máximos, en cambio, muestran peticiones aisladas de 10 a 13 segundos en las muestras grandes.

No atribuyo automáticamente esos máximos a los http_500: para hacerlo habría que confirmar que el cálculo de latencia incluye las peticiones fallidas y correlacionar cada máximo con el registro correspondiente. También podrían ser respuestas exitosas anormalmente lentas, un evento de calentamiento o jitter de la plataforma.

El dato defendible es más sencillo: los máximos no arrastran de forma equivalente a la mediana, y por tanto parecen eventos aislados. Para caracterizar realmente la cola bajo SLA faltaría repetir las ejecuciones y, sobre todo, medir con concurrencia y carga sostenida.

Fiabilidad observada

Los dos agentes presentan tasas de éxito prácticamente idénticas:

  • Phase B: un http_500 en N=100 y otro en N=1000 para cada implementación.
  • Phase A: un http_500 en C# para N=1000; ninguno en Python.

Con tan pocos fallos no existe evidencia suficiente para distinguir la fiabilidad de los dos stacks. Además del resultado final, una prueba futura debería publicar el éxito al primer intento y el número de reintentos recuperados; de lo contrario, un 200 después de un 429 puede ocultar presión real sobre la plataforma.

El consumo de tokens aparece como n/a, error mio en el codigo, si lo repitiera otra vez, tendría que esperar unas 5 horas.

Latencia no es throughput

Los resúmenes originales incluyen una métrica llamada throughput_rps. Su valor coincide aproximadamente con la inversa de la latencia media: tasa secuencial teórica = 1/latencia media

Para N=1000:

Fase C# Python Relación C#/Python
Phase B 0,282 req/s 0,214 req/s 1,31x
Phase A 0,296 req/s 0,219 req/s 1,35x

Estos números son útiles como tasa secuencial de servicio equivalente, pero no son el throughput máximo observado. El generador estaba limitado a TargetRps=0.15 y solo había una petición en vuelo.

Por eso no concluyo que C# haya demostrado un 31 % más de capacidad bajo carga. Concluyo algo más acotado: si cada worker procesara estrictamente una petición cada vez y la relación de latencias se mantuviera, el tiempo de servicio observado implicaría una capacidad secuencial teórica 1,31 veces mayor en Phase B.

Para hablar de throughput real habría que aumentar la concurrencia hasta encontrar el punto de saturación de cada implementación.

Un modelo de capacidad, no una promesa

La relación de Phase B con N=1000 es:

4664,65 / 3548,64=1,3145

Bajo un modelo simplificado —misma concurrencia útil por instancia, escalado lineal y ausencia de otros límites— Python necesitaría aproximadamente 1,31 veces la capacidad concurrente de C# para compensar el tiempo de servicio observado.

Workers equivalentes C# Workers Python según el modelo
50 66
100 131
500 657

Esta tabla no representa instancias reales provisionadas por Foundry. Es una traducción matemática de la relación de latencias. En producción pueden intervenir CPU, memoria, pooling de conexiones, límites internos del SDK, autoscaling, cuotas, batching y concurrencia por réplica. Cualquiera de esos factores puede ampliar o reducir la diferencia.

El ángulo FinOps, con los supuestos visibles

También puede construirse un modelo económico de primer orden. Supongamos, únicamente para normalizar la comparación:

  • Una unidad de compute cuesta 0,20 $/hora.
  • Cada petición ocupa esa unidad durante toda la latencia media.
  • No hay concurrencia interna, tiempos ociosos ni descuentos.
  • El coste del modelo, tokens, red, almacenamiento y observabilidad queda fuera.
  • La relación se mantiene al escalar.

Con N=1000 en Phase B, el compute equivalente por millón de peticiones sería:

Stack Tiempo medio Compute equivalente Coste hipotético por 1 M de peticiones
C#/.NET 3,549 s 986 horas 197 $
Python 4,665 s 1296 horas 259 $
Diferencia 1,116 s 310 horas 62 $

Aplicado mecánicamente a distintos volúmenes:

Volumen mensual C# Python Diferencia hipotética
10 millones 1970 $ 2590 $ 620 $/mes
100 millones 19 700 $ 25 900 $ 6200 $/mes
1000 millones 197 000 $ 259 000 $ 62 000 $/mes

No son precios oficiales de Hosted Agents ni una predicción de la factura. Son una forma de expresar el coste de ocupación que tendría la diferencia de latencia si el servicio se facturase y escalase conforme a esos supuestos.

El modelo sirve para decidir si merece la pena realizar la siguiente prueba. Si el volumen es pequeño, la diferencia económica puede ser irrelevante frente a la productividad del equipo. Si el volumen es enorme, una señal de 1,31x justifica medir capacidad y coste reales antes de cerrar la arquitectura.

Cuándo elegiría C#/.NET

Estos resultados hacen especialmente interesante C# cuando el sistema tiene alguno de estos perfiles:

  • Respuestas cortas, modelos pequeños o caminos deterministas donde el runtime y el hosting representan una parte apreciable de la latencia.
  • Integración fuerte con servicios y librerías del ecosistema .NET.
  • Equipos capaces de operar y mantener el stack sin pagar un coste organizativo de adopción.
  • SLA de latencia donde una diferencia sostenida de aproximadamente un segundo pueda importar.
  • Volumen suficiente para justificar una prueba real de capacidad y FinOps.

El benchmark no demuestra todavía que C# necesite menos instancias bajo saturación. Sí ofrece evidencia suficiente para considerar esa hipótesis y medirla.

Cuándo Python sigue siendo la elección correcta

Python continúa siendo una opción perfectamente razonable cuando:

  • El modelo domina el tiempo total con generaciones largas.
  • El agente se apoya en librerías de datos, ML o IA con mejor soporte en Python.
  • La productividad y experiencia del equipo pesan más que un segundo de latencia.
  • El volumen es moderado y la diferencia de infraestructura sería menor que el coste de reescritura o especialización.
  • El agente solo coordina servicios externos y no constituye el cuello de botella real.

La decisión no debería reducirse a “qué lenguaje ganó”. Si el cuello de botella está en el modelo, en una API externa o en la arquitectura de datos, optimizar el runtime puede ser trabajar sobre la pieza equivocada.

Qué falta para hablar de alta demanda

La continuación natural del experimento es una prueba de capacidad. Para sostener conclusiones sobre alta demanda ejecutaría:

  • Concurrencias 1, 2, 4, 8, 16 y 32.
  • RPS creciente hasta encontrar saturación.
  • Varias repeticiones por configuración y en momentos diferentes.
  • Orden C#/Python aleatorizado dentro de cada par.
  • Warm y cold starts separados.
  • CPU, memoria y reinicios del contenedor.
  • Latencia p50, p95 y p99 bajo carga.
  • Throughput real calculado sobre el tiempo total de la prueba.
  • 429, 5xx, reintentos y éxito al primer intento.
  • Consumo y coste real de la plataforma cuando esté disponible.

Ahí sí podremos responder cuánto escala cada implementación, dónde se rompe y cuánto cuesta mantener el mismo SLA.

El estilo de estos  benchmark: https://programming-language-benchmarks.vercel.app/python-vs-csharp , junto con muchos otros, desmontan una idea repetida con demasiada ligereza: «Python es el lenguaje de la IA». Python posee el ecosistema de IA más amplio, pero eso no lo convierte automáticamente en la mejor opción para ejecutar cualquier solución. Cuando entran en juego el rendimiento, la concurrencia, el consumo de recursos, la integración corporativa o la operación a escala, otros lenguajes pueden resultar más adecuados. Como casi todo en ingeniería, la respuesta no es un dogma, sino un incómodo pero necesario: depende. 

La lectura que me llevo

No es un veredicto universal de C# contra Python. Es un resultado concreto de dos Hosted Agents, dos stacks y una carga controlada.

En baja carga y con una sola petición en vuelo, C#/.NET mostró entre un 22 % y un 30 % menos de latencia media. La diferencia es consistente en esta serie, aparece con y sin llamada al modelo y ronda 1–1,4 segundos por petición.

Eso ya es una señal arquitectónica relevante. Lo que todavía no es es una prueba de throughput, escalabilidad o factura. Para convertirla en una decisión de capacidad falta someter ambos agentes a concurrencia creciente y medir el punto de saturación.

Un benchmark útil no debe darte una religión ni un titular más grande que sus datos. Debe darte una observación reproducible, dejar visibles sus límites y decirte cuál es el siguiente experimento que merece la pena ejecutar.