---
{
  "title": "Rendimiento a escala: punto de referencia inicial",
  "description": "Una nota de referencia para nuestra serie sobre rendimiento a escala: qué medimos primero, por qué el punto de partida importa y cómo mantenemos la honestidad del benchmark.",
  "date": "2026-06-29",
  "author": "Marat Surmashev",
  "reading-time": "6 min read",
  "tags": ["Database", "Infrastructure", "Aidbox", "Performance"]
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---

## Por qué importa el punto de referencia

Antes de comparar servidores FHIR bajo carga, necesitamos un punto de partida limpio. El punto de referencia nos indica cómo se comportan los sistemas con una base de datos vacía (o casi vacía) y nos proporciona una referencia para todo lo que viene después: cuando el volumen de datos y el tráfico empiezan a importar.

Como indicamos en nuestra [publicación anterior](/articles/performance-at-scale), nos centramos en las cargas de trabajo FHIR principales: CRUD, procesamiento de Bundle y Search. Todo el benchmark es de **código abierto**: el código fuente, el arnés de pruebas y los resultados se encuentran en [github.com/HealthSamurai/fhir-server-performance-benchmark](https://github.com/HealthSamurai/fhir-server-performance-benchmark), y la ejecución se repite diariamente, por lo que el [informe interactivo](https://healthsamurai.github.io/fhir-server-performance-benchmark/) (todos los gráficos más los datos brutos) refleja siempre los últimos números. Las cifras citadas en esta publicación corresponden a la ejecución del 28 de junio de 2026; el informe en vivo es la fuente de verdad.

Probamos cuatro servidores FHIR: Aidbox, HAPI FHIR, Medplum y el Microsoft FHIR Server. Abarcan tres entornos de ejecución distintos y dos bases de datos diferentes. Aidbox y HAPI se ejecutan ambos sobre la JVM con PostgreSQL; Medplum se ejecuta sobre Node.js, también con PostgreSQL; y el Microsoft FHIR Server se ejecuta sobre .NET y es el caso atípico en almacenamiento: no admite PostgreSQL, por lo que ejecutamos la versión más reciente del Microsoft FHIR Server sobre SQL Server.

| Servidor | Base de código | Entorno de ejecución | Base de datos |
|--------|---------------|----------------|-------------|
| [Aidbox](https://www.health-samurai.io/fhir-server)    | código cerrado | JVM (Clojure) | PostgreSQL  |
| [HAPI FHIR](https://github.com/hapifhir/hapi-fhir)  | código abierto | JVM (Java)     | PostgreSQL  |
| [Medplum](https://github.com/medplum/medplum)    | código abierto | Node.js        | PostgreSQL  |
| [Microsoft FHIR Server](https://github.com/microsoft/fhir-server)    | código abierto | .NET (C#)      | SQL Server  |

El Microsoft FHIR Server es la incorporación más reciente al benchmark, y los resultados que se muestran a continuación lo incluyen junto a los tres servidores respaldados por PostgreSQL.


## Entorno de pruebas

Nuestras pruebas se ejecutan en una única máquina bare-metal con 64 núcleos de CPU y 500 GB de RAM. Todo el stack se orquesta con Docker Compose, y los recursos se fijan por contenedor para que la comparación sea justa:

| Servidor | Imagen | CPU / RAM | Topología |
|--------|-------|-----------|----------|
| Aidbox | `healthsamurai/aidboxone:edge` | 8 vCPU / 24 GB | instancia única (JVM) |
| HAPI FHIR | `hapiproject/hapi:latest` | 8 vCPU / 24 GB | instancia única (JVM) |
| Medplum | `medplum/medplum-server:latest` | 1 vCPU / 3 GB cada una | 8 réplicas (Node.js) |
| Microsoft FHIR Server | `mcr.microsoft.com/healthcareapis/r4-fhir-server:latest` | 8 vCPU / 24 GB | instancia única (.NET) |

Cada servidor de aplicaciones dispone del mismo presupuesto: 8 vCPU y 24 GB de RAM. Medplum lo alcanza de forma diferente: su entorno de ejecución Node.js es monohilo, por lo que un único proceso no puede utilizar 8 núcleos; se escala como 8 réplicas de 1 vCPU / 3 GB cada una (8 vCPU y 24 GB en total) para competir en igualdad de condiciones. Medplum depende además de Redis para sesiones y caché, algo que los demás no necesitan.

Para los tres servidores basados en PostgreSQL, la capa de base de datos es **PostgreSQL 18** (8 vCPU / 30 GB), compartida entre el stack pero aislada con un modelo de base de datos por servidor, de modo que el almacenamiento o la carga de consultas de un servidor no afecten a los demás. El Microsoft FHIR Server no puede ejecutarse sobre PostgreSQL, por lo que dispone de su propio **SQL Server 2022** dedicado (edición Developer) con el mismo presupuesto de 8 vCPU / 30 GB: el equivalente en SQL Server al Postgres compartido. Dado que las suites se ejecutan de forma secuencial, en cada momento solo hay activo un servidor de aplicaciones y su base de datos. Todo se ejecuta sobre almacenamiento NVMe SSD local para eliminar la latencia de red de los resultados y mantener el conjunto de datos cerca del servidor.

Para la generación de carga utilizamos [Grafana k6](https://k6.io/), que ejecuta los escenarios en secuencia (prewarm → CRUD → import → search). Para los datos sintéticos utilizamos [Synthea](https://github.com/synthetichealth/synthea) porque produce patrones de datos sanitarios realistas. El uso de CPU, memoria e I/O por contenedor se recopila con cAdvisor, mientras que los datos internos de la base de datos provienen de postgres-exporter (para los servidores PostgreSQL) y de mssql-exporter (para el SQL Server del Microsoft FHIR Server). Todo se agrega mediante Prometheus y se visualiza en Grafana: las mismas métricas que respaldan los gráficos a continuación. Los detalles completos están en la [página de infraestructura](https://healthsamurai.github.io/fhir-server-performance-benchmark/infrastructure/) del informe.

> **Una nota sobre la equidad.** Nosotros desarrollamos Aidbox, por lo que naturalmente lo conocemos mejor. *No* somos expertos en HAPI FHIR, Medplum ni en el Microsoft FHIR Server, y es perfectamente posible que nuestra configuración para ellos no sea óptima. Nuestro objetivo fue el opuesto al cherry-picking: maximizar la utilización del hardware y proporcionar a cada servidor los recursos más equitativos que razonablemente pudiéramos. El setup completo es de código abierto precisamente por esta razón: si sabe cómo afinar alguno de estos servidores de manera más efectiva, nos gustaría saberlo. Abra una issue o un pull request en [el repositorio del benchmark](https://github.com/HealthSamurai/fhir-server-performance-benchmark) y la ejecución diaria incorporará sus cambios.

## Suites y escenarios de prueba

Las pruebas de rendimiento son fundamentales para comprender el comportamiento del sistema bajo carga. Para esta referencia inicial ejecutamos tres suites, en orden:

1. **CRUD sobre una base de datos vacía**
   - Medir el rendimiento de Create, Read, Update y Delete sobre una instalación nueva
2. **Importación por lotes (1K pacientes)**
   - Realizar una importación por lotes de 1.000 registros de pacientes sintéticos (Synthea) y medir el rendimiento y el almacenamiento
3. **Search**
   - Evaluar el rendimiento de las operaciones de búsqueda sobre el conjunto de datos importado

Esto nos proporciona un punto de referencia limpio. Los conjuntos de datos más grandes y la carga incremental, donde la degradación de escrituras y las consultas complejas empiezan a importar, son el objeto de las siguientes publicaciones de la serie.

## Rendimiento CRUD de referencia

La suite CRUD contiene operaciones secuenciales de Create, Read, Update y Delete sobre nueve recursos FHIR diferentes: Patient, Location, Practitioner, Organization, Encounter, MedicationRequest, Observation, Claim y ExplanationOfBenefit. La suite se ejecuta con 300 hilos concurrentes constantes durante un máximo de 5 minutos.

Establecemos las métricas de referencia con el RPS promedio sobre todas las iteraciones.

__Rendimiento promedio (RPS) en todas las operaciones CRUD (mayor es mejor)__
![CRUD operations RPS on empty database](total_rps.svg)

| Servidor | Rendimiento CRUD total (RPS) |
|--------|-----------------------------|
| Aidbox | 5.212 |
| HAPI   | 3.058 |
| Medplum | 1.420 |
| Microsoft FHIR Server | 440 |

__Latencia promedio (percentil 99) por operación (menor es mejor)__
![CRUD operations p99 latency by operation](describe_rps.svg)

| Operación | Aidbox | HAPI | Medplum | Microsoft |
|-----------|--------|------|---------|-----------|
| Create | 106 ms | 276 ms | 758 ms | 1.180 ms |
| Read   | 91 ms | 225 ms | 404 ms | 379 ms |
| Update | 110 ms | 271 ms | 626 ms | 1.233 ms |
| Delete | 93 ms | 239 ms | 647 ms | 1.064 ms |

A partir de estos resultados, Aidbox ofrece aproximadamente un 70% más de rendimiento que HAPI y unas 3,7 veces más que Medplum. El Microsoft FHIR Server queda muy por detrás en esta suite, con 440 RPS, más de un orden de magnitud por debajo de Aidbox. En cuanto a latencia, Aidbox es aproximadamente 2,5 veces más rápido que HAPI y 6 veces más rápido que Medplum en el percentil 99. El Microsoft FHIR Server es un caso interesante: sus lecturas son rápidas (379 ms p99, incluso por delante de Medplum), pero sus escrituras son las más lentas del grupo (~1,2 s p99 en create y update), lo que arrastra su rendimiento global hacia abajo.

Un aspecto a tener en cuenta al leer estas cifras: la brecha de rendimiento (≈70% sobre HAPI) es menor que la brecha de latencia (≈2,5x). Bajo alta concurrencia, un servidor puede mantener el rendimiento agregado procesando muchas solicitudes en paralelo incluso cuando cada solicitud individual es más lenta, por lo que la latencia de cola y el RPS total no necesariamente se mueven juntos: una comparación de un único número puede ocultar cuál es el que realmente importa para una carga de trabajo determinada.

__Latencia promedio en todas las operaciones (percentil 99) por tamaño de recurso (menor es mejor)__
![CRUD p99 latency by resource size](avg_latency_by_resource_size.svg)

El gráfico muestra que Aidbox presenta una ligera correlación entre latencia y tamaño de recurso, más visible al procesar recursos Encounter, Patient y ExplanationOfBenefit. HAPI muestra una latencia más constante entre los diferentes tamaños de recurso, mientras que Medplum y el Microsoft FHIR Server son uniformemente lentos en las escrituras. También observamos un pico interesante en el procesamiento de Patient por parte de Medplum (su p99 de create salta a ~1,16 s, frente a ~560–830 ms para otros recursos), lo que sugiere que se realiza algún trabajo adicional específicamente para el recurso Patient.

## Procesamiento por lotes

Para probar las capacidades de procesamiento por lotes, utilizamos un conjunto de datos de 1.000 registros de pacientes sintéticos generados con Synthea (alrededor de 2 millones de recursos en total). Los FHIR Bundles generados varían en tamaño, desde bundles pequeños de unos 150 KB hasta bundles grandes de hasta 120 MB y hasta 50.000 recursos en un único bundle. Cada Bundle es de tipo `transaction`. Para que la prueba sea más realista y se aproxime al uso en el mundo real, ejecutamos 20 hilos concurrentes procesando estos bundles.

__Rendimiento promedio (recursos por segundo) por servidor (mayor es mejor)__
![1,000 synthetic patient records import throughput](1k_import_throughput.svg)

| Servidor | Rendimiento de importación (recursos/seg) |
|--------|-----------------------------------|
| Aidbox | 2.678 |
| HAPI   | 2.214 |
| Medplum | 764 |
| Microsoft FHIR Server | 448 |

En el procesamiento por lotes, Aidbox y HAPI lideran e intercambian el primer puesto entre ejecuciones: en esta, Aidbox va por delante, ingiriendo 2.678 recursos por segundo frente a los 2.214 de HAPI (aproximadamente un 21% más rápido). Medplum queda muy por detrás de ambos con 764 recursos por segundo, y el Microsoft FHIR Server es el más lento con 448 recursos por segundo, unas 5–6 veces por detrás de los dos servidores JVM.

__Tamaño de la base de datos por servidor (menor es mejor)__
![1,000 synthetic patient records import database size](1k_import_db_size.svg)

El almacenamiento es donde los cuatro servidores se distancian más, formando dos grupos aproximados. El Microsoft FHIR Server (4,24 GB) y Aidbox (6,83 GB) se mantienen compactos; Medplum (11,8 GB) y HAPI (22,6 GB) son notablemente más grandes: la huella de HAPI es aproximadamente 3,3 veces la de Aidbox. Los dos servidores compactos llegan a ese resultado por caminos distintos: Aidbox por no pre-construir índices, el Microsoft FHIR Server sobre su almacén de datos SQL Server; sin embargo, como hemos visto anteriormente, el servidor de Microsoft paga el precio en otro lugar, con escrituras y búsquedas lentas. Volvemos al equilibrio de los índices en la conclusión.

## Suite de Search

Hacer un benchmark exhaustivo de la funcionalidad de búsqueda en un servidor FHIR es un reto complejo por varias razones:

1. FHIR dispone de un extenso conjunto de parámetros de búsqueda, lo que hace que probarlos todos requiera mucho tiempo.
2. Existen muchas combinaciones de tipo de parámetro de búsqueda y tipo de valor.
3. Se pueden aplicar a las búsquedas diversos modificadores y prefijos.
4. Hay operaciones complejas como joins (`_include`, `_revinclude`, `_has`, encadenadas) y ordenación.
5. El número de combinaciones posibles de parámetros de búsqueda es enorme.

Dado que probar todas las combinaciones posibles de parámetros de búsqueda no es práctico, nos centramos en los parámetros de búsqueda más utilizados. Esto nos proporciona una comprensión de referencia sólida sobre la eficiencia con que cada servidor implementa su lógica de búsqueda. Nos concentramos en los parámetros de búsqueda FHIR R4 estándar que se utilizan frecuentemente en la práctica y que tienen datos correspondientes en nuestro conjunto de datos sintético, cubriendo seis familias: string, date, reference, quantity, token y parámetros composite de FHIR.

Ejecutamos la suite con 30 hilos concurrentes (k6 VUs) durante 2 minutos, utilizando un tamaño de página fijo de 20 resultados (`_count=20`). Dado que las operaciones de búsqueda pueden devolver conjuntos de resultados muy grandes, excluimos el tiempo de transferencia y análisis de la respuesta de las mediciones habilitando la configuración `discardResponseBodies: true` de k6: nos centramos únicamente en el tiempo de respuesta del servidor, no en la conformidad con FHIR. Todas las familias de búsqueda se ejecutan mezcladas en una única iteración en lugar de secuencialmente, lo que se aproxima más al uso real. Cada familia también lanza un valor deliberadamente no coincidente (un id, código o nombre inexistente) para ejercitar la ruta de resultado vacío, y los valores de referencia se muestrean en vivo de cada servidor (ids de recursos reales) justo antes de la ejecución.

Una advertencia derivada de descartar los cuerpos de respuesta: medimos el tiempo de respuesta, no la corrección. Un servidor que gestiona mal un tipo de consulta (devolviendo un error, o un resultado sin filtrar, rápidamente) puede parecer artificialmente rápido. Señalamos el único caso que encontramos (la búsqueda composite de Medplum, a continuación), pero el mismo escepticismo aplica en general: trate cualquier latencia inusualmente baja para un servidor y tipo determinados como algo que conviene verificar en los resultados en vivo, en lugar de tomarlo al pie de la letra.

Los parámetros que ejercitamos:

| Tipo de búsqueda | Recurso | Parámetros |
|-------------|----------|------------|
| String | Patient | `name`, `address` (con `:contains`) |
| String | Organization | `name` (con `:contains`) |
| Date | Patient | `birthdate` |
| Date | Observation | `date` |
| Date | Encounter | `date` |
| Reference | Observation | `subject`, `encounter`, `performer` |
| Reference | Encounter | `subject`, `participant` |
| Reference | MedicationRequest | `subject`, `encounter`, `requester` |
| Quantity | Observation | `value-quantity`, `component-value-quantity`, `combo-value-quantity` |
| Token | Observation | `category`, `code` |
| Token | Encounter | `status`, `class` |
| Composite | Observation | `code-value-quantity`, `component-code-value-quantity`, `combo-code-value-quantity` |

Las búsquedas de date y quantity rotan a través de prefijos (`eq`, `lt`, `gt`, `ge`, `le`; date añade además `sa` y `eb`). Las búsquedas de string utilizan el modificador `:contains`. Las búsquedas de token cubren `[code]` simple, `[system]|[code]` completamente cualificado y listas OR separadas por comas. Las búsquedas composite utilizan parámetros composite de FHIR con el separador `$` (por ejemplo `code-value-quantity=8867-4$gt100`).

### Resultados de Search

__Rendimiento total de búsqueda (RPS, mayor es mejor)__
![Search throughput by server](search_throughput.svg)

| Servidor | Rendimiento de búsqueda (RPS) |
|--------|-------------------------|
| Aidbox | 3.404 |
| Medplum | 1.796 |
| HAPI   | 1.005 |
| Microsoft FHIR Server | 261 |

__Latencia P99 por tipo de búsqueda (ms, menor es mejor)__
![Search p99 latency by type](search_latency.svg)

| Tipo de búsqueda | Aidbox | Medplum | HAPI | Microsoft |
|-------------|--------|---------|------|-----------|
| String    | 24 | 81 | 77  | 188 |
| Date      | 26 | 82 | 121 | 271 |
| Reference | 24 | 83 | 96  | 167 |
| Token     | 32 | 82 | 97  | 197 |
| Quantity  | 55 | 91 | 101 | 1.191 |
| Composite | 47 | — | 125 | 1.897 |

*El gráfico muestra los cuatro servidores. Los p99 de quantity (~1,2 s) y composite (~1,9 s) del Microsoft FHIR Server se salen de la escala y comprimen los tres servidores PostgreSQL en barras cortas cerca de la línea de base; sus valores exactos figuran en la tabla anterior. Medplum está ausente del grupo composite porque no admite ese tipo de búsqueda (véase más abajo).*

El orden cambia en comparación con CRUD. En búsqueda, Aidbox lidera el rendimiento con 3.404 RPS, aproximadamente un 90% más que Medplum (1.796 RPS) y unas 3,4 veces más que HAPI (1.005 RPS). El Microsoft FHIR Server queda muy por detrás con 261 RPS. Cabe destacar que HAPI, que ocupaba el segundo lugar en CRUD, es el más lento de los tres servidores PostgreSQL en búsqueda, tanto en rendimiento como en latencia de cola.

En cuanto a latencia, Aidbox registra el p99 más bajo en todas las familias de búsqueda. Medplum se sitúa de forma consistente en la posición intermedia, excepto en composite, que no admite (véase la nota a continuación). Entre los servidores Postgres, la latencia de HAPI es la más alta, rondando los 95–125 ms en todas las familias. El Microsoft FHIR Server opera en un régimen completamente diferente: aceptable en búsquedas de string, date, reference y token (~170–270 ms), pero extremadamente lento en búsquedas de quantity (~1,2 s) y composite (~1,9 s), que es precisamente lo que hunde su rendimiento de búsqueda.

> **Medplum no admite búsqueda composite.** La [documentación de arquitectura de búsqueda](https://www.medplum.com/docs/contributing/search-architecture) de Medplum indica que el tipo de parámetro `composite` de FHIR no está soportado. En el benchmark, estas solicitudes regresan sin realizar la búsqueda, por lo que cualquier latencia o rendimiento que reporten es un artefacto y no una medición real; de ahí que Medplum pareciera «superar» a todos en composite en borradores anteriores. Por tanto, dejamos en blanco la celda composite de Medplum y la excluimos del gráfico. La búsqueda de quantity, en cambio, *sí* está soportada por Medplum, por lo que esas cifras se mantienen.

Una nota sobre el alcance: esta referencia inicial no incluye ordenación (`_sort`) ni operaciones join (`_include`, `_revinclude`, `_has`, encadenadas). Son más interesantes una vez que el volumen de datos crece; en un conjunto de datos de 1K pacientes casi todo cabe en memoria, por lo que las añadiremos sobre conjuntos de datos más grandes en una publicación posterior.

## Conclusión

Con una referencia inicial limpia y mayormente en memoria, la clasificación queda así; cambia algo entre ejecuciones diarias, así que léala como una instantánea, no como un veredicto:

- **CRUD** — Aidbox lidera el rendimiento (5.212 RPS) por delante de HAPI (3.058) y Medplum (1.420), con la mejor latencia p99 en todas las operaciones (~100 ms frente a ~250 ms de HAPI y ~610 ms de Medplum). El Microsoft FHIR Server se queda atrás con 440 RPS, con lecturas rápidas pero ~1,2 s de latencia en escritura.
- **Importación por lotes** — Aidbox lidera la ingestión en esta ejecución (2.678 frente a los 2.214 recursos/seg de HAPI); Medplum queda muy por detrás (764), y el Microsoft FHIR Server es el más lento (448).
- **Search** — Aidbox lidera tanto el rendimiento (3.404 RPS) como la latencia por consulta; HAPI cae al último lugar entre los servidores Postgres, y el Microsoft FHIR Server queda muy por detrás en rendimiento (261 RPS), hundido por consultas de quantity y composite extremadamente lentas.
- **Almacenamiento** — dos grupos: el Microsoft FHIR Server (4,24 GB) y Aidbox (6,83 GB) son compactos, mientras que Medplum (11,8 GB) y HAPI (22,6 GB) son más grandes: la huella de HAPI es aproximadamente 3,3 veces la de Aidbox.

### Estrategia de indexación: un equilibrio que esta referencia solo mide a medias

Las cifras de almacenamiento e importación derivan directamente de cómo cada servidor almacena e indexa los datos, y esa elección es un equilibrio, no un veredicto.

HAPI, Medplum y el Microsoft FHIR Server pre-construyen índices en los campos con capacidad de búsqueda a medida que se escriben los datos. Esto tiene un coste en el rendimiento de escritura y, en el caso de HAPI y Medplum, en el almacenamiento (los 12–23 GB mencionados anteriormente). El Microsoft FHIR Server demuestra que el coste de almacenamiento no es inevitable: pre-construye índices y aun así se mantiene en 4,24 GB, pero paga un precio muy alto en latencia de escritura.

Aidbox adopta el enfoque contrario por defecto, y tiene sus dos caras. Se distribuye **sin ningún índice de búsqueda**, lo que es lo que hace que sus importaciones sean rápidas y su huella pequeña, pero también significa que una consulta sin indexar recurre a un escaneo secuencial, y la responsabilidad de la indexación recae sobre usted. No es una tarea trivial: hay que saber qué parámetros de búsqueda utiliza realmente la carga de trabajo, y una indexación excesiva recupera exactamente los costes de escritura y almacenamiento que acabamos de describir. Lo que Aidbox ofrece en lugar de índices pre-construidos es el conjunto de herramientas para hacerlo de forma deliberada: analíticas de consultas, estadísticas sobre qué parámetros de búsqueda se utilizan realmente y recomendaciones de índices derivadas del tráfico real. Bien utilizado, eso permite indexar de forma precisa para su carga de trabajo y obtener tanto operaciones rápidas como un conjunto de datos muy compacto y almacenado eficientemente; utilizado sin cuidado, se obtiene un servidor sin índices. Es un valor predeterminado más potente en manos expertas y un filo más cortante en manos inexpertas.

La advertencia importante, y la razón para leer con atención los números de búsqueda anteriores, es que pre-construir índices ofrece algo a cambio: un rendimiento de consulta predecible a medida que crecen los datos. Un conjunto de datos de 1K pacientes que cabe cómodamente en memoria es precisamente el caso donde ese beneficio no se manifiesta, por lo que esta referencia inicial favorece estructuralmente al valor predeterminado sin índices en búsqueda. Si esa ventaja se mantiene a medida que crecen los datos, y si la degradación en tiempo de escritura, no solo la velocidad bruta, empieza a dominar, es exactamente lo que la siguiente publicación está diseñada para medir.

## Lo siguiente en la serie

Este es el punto de partida. En las siguientes publicaciones pasamos de la referencia inicial a cargas de trabajo más pesadas: conjuntos de datos más grandes y carga incremental, para medir cómo se degrada la velocidad de importación a medida que se acumulan datos, cómo aguantan CRUD y las búsquedas complejas (ordenación y joins) sobre una base de datos completa, y cómo se proyecta la brecha de almacenamiento en volúmenes de datos realistas.

*Síganos en [LinkedIn](https://www.linkedin.com/company/health-samurai) para recibir la próxima actualización del benchmark.*