Prueba de carga de una API GraphQL: guía práctica
7 min de lectura
Para hacer una prueba de carga de una API GraphQL, escribe un script con las operaciones que tus clientes envían de verdad, da a cada una su propio nombre en la herramienta de pruebas y mézclalas en las proporciones que ves en producción, con variables variadas. Revisa en cada cuerpo de respuesta si hay un array errors, porque un servidor GraphQL suele responder con HTTP 200 aunque parte de la operación haya fallado. Después sube la carga y juzga cada operación por sus propios percentiles.
Fuentes: graphql.org, el borrador de la especificación GraphQL over HTTP y la documentación de cada herramienta, todo comprobado el 9 de octubre de 2026.
| Herramienta | Nombrar cada operación con | Hacer fallar una respuesta con errors | Alimentar variables desde |
|---|---|---|---|
| k6 | tags: { name: "..." } | Un check() sobre el cuerpo parseado | SharedArray |
| JMeter | El nombre del sampler | JSON Assertion sobre $.errors, invertida | CSV Data Set Config |
| Gatling | http("...") o el plugin de GraphQL | jsonPath("$.errors").notExists() | Feeders |
| Artillery | Un name en la petición | notHasProperty: "errors" | config.payload |
¿Por qué una API GraphQL necesita una prueba de carga distinta?
Porque todas las operaciones van a una sola URL y el trabajo que hay detrás depende de la consulta y de sus variables. Un informe agrupado por URL mete en el mismo saco una lectura barata y una búsqueda cara.
La guía Serving over HTTP de graphql.org dice que un servidor GraphQL funciona sobre un único endpoint, normalmente /graphql. Las operaciones llegan ahí por POST, con un cuerpo JSON que lleva query, un operationName opcional y variables.
¿Cómo se obtienen resultados por operación?
Pon a cada petición el nombre de su operación GraphQL y haz que la herramienta agrupe sus estadísticas por ese nombre. Cada herramienta tiene su propio ajuste para esto (comprobado el 9 de octubre de 2026).
- k6: la guía de tags y grupos indica que se defina
tags.namepara dar a una petición un nombre con sentido, y los thresholds pueden apuntar a un solo tag, comohttp_req_duration{name:GetProduct}. - JMeter: el Aggregate Report crea una fila por cada petición con un nombre distinto. Desde JMeter 5.4, el sampler GraphQL HTTP Request tiene campos para Query, Variables y Operation Name.
- Gatling: el nombre de la petición es la clave de sus estadísticas, y el plugin de GraphQL nombra las peticiones según la operación, por ejemplo
query GetUser. - Artillery: el plugin metrics-by-endpoint informa por URL y añade a cada entrada el
namede la petición, así que las operaciones con nombre aparecen por separado;useOnlyRequestNames: truequita la URL y deja solo el nombre.
¿Qué operaciones debe enviar la prueba?
Las que recibe producción, en las mismas proporciones. Saca el recuento de cada operación de los logs del servidor o de tu monitorización de GraphQL y conviértelo en una mezcla ponderada de lecturas y mutaciones.
Usa los documentos que envían tus clientes, con su profundidad real, en lugar de una consulta simplificada. Alimenta las variables desde un archivo de datos, para que el servidor busque muchos registros distintos en vez de devolver un único resultado en caché. Mantén también realista el número de operaciones distintas: la nota técnica de Apollo sobre pruebas de carga de GraphQL, un documento del propio fabricante, explica que cada operación nueva se analiza y se valida antes de ejecutarse, y desaconseja generar muchas más operaciones únicas de las que envían los clientes reales.
Las mutaciones cambian datos, así que ejecútalas donde puedas restaurarlos. La guía paso a paso de pruebas de carga explica los objetivos y la rampa de subida.
¿Por qué una respuesta HTTP 200 puede ser un fallo?
Porque GraphQL informa de los errores dentro del cuerpo de la respuesta. Una petición puede devolver el estado 200 con un array errors y datos parciales, y una comprobación del estado la da por buena.
La página Response de graphql.org explica que, cuando falla un resolver, la ejecución sigue y la respuesta trae data junto a errors. Su guía de HTTP añade que esa respuesta debe recibir aun así un estado 2xx, porque HTTP no tiene un código para el éxito parcial, y que algunos servidores antiguos responden 2xx incluso ante errores de validación. El borrador GraphQL over HTTP propone un estado propio, el 294, para el éxito parcial con el tipo de medio más reciente application/graphql-response+json.
En una prueba de carga esto perjudica dos veces. Los errores no se cuentan, y las peticiones que fallan rápido, como operaciones rechazadas por un límite de profundidad, rebajan los tiempos de respuesta.
¿Cómo se comprueba el array errors en cada herramienta?
Parsea el cuerpo JSON y haz fallar la petición cuando haya errors. Cada herramienta tiene una forma integrada de hacerlo.
En k6 basta con un check sobre el cuerpo parseado, y un threshold sobre la métrica checks hace fallar la ejecución, porque los checks por sí solos no cambian el código de salida de k6:
import http from "k6/http";
import { check, sleep } from "k6";
import { SharedArray } from "k6/data";
const URL = "https://staging.example.com/graphql";
const ids = new SharedArray("ids", () =>
JSON.parse(open("./product-ids.json")));
const QUERY = `query GetProduct($id: ID!) {
product(id: $id) { id name price }
}`;
export const options = {
vus: 20,
duration: "5m",
thresholds: {
checks: ["rate>0.99"],
"http_req_duration{name:GetProduct}": ["p(95)<400"],
},
};
function parse(res) {
try { return res.json(); } catch (e) { return null; }
}
export default function () {
const id = ids[Math.floor(Math.random() * ids.length)];
const payload = JSON.stringify({
query: QUERY,
operationName: "GetProduct",
variables: { id: id },
});
const res = http.post(URL, payload, {
headers: { "Content-Type": "application/json" },
tags: { name: "GetProduct" },
});
const body = parse(res);
check(res, {
"status is 2xx": (r) => r.status >= 200 && r.status < 300,
"no GraphQL errors": () => body !== null && !body.errors,
});
sleep(1);
}
SharedArray carga los ID una sola vez para todos los usuarios virtuales. El try evita que un cuerpo que no es JSON, como la página de error de un proxy, corte la iteración.
En JMeter, añade una JSON Assertion con la ruta $.errors y marca “Invert assertion”, para que la muestra falle cuando la ruta exista. Un JSON Extractor copia ID en variables para la siguiente operación.
En Gatling, añade .check(jsonPath("$.errors").notExists()), ambos de su referencia de checks. El plugin de GraphQL comprueba el estado y los errores por defecto, pero su página de instalación limita las ejecuciones con la Community Edition a 5 usuarios y 5 minutos (comprobado el 9 de octubre de 2026). Para pruebas más grandes sin Gatling Enterprise, usa peticiones HTTP normales con ese check.
En Artillery, el plugin expect ofrece notHasProperty: "errors", y reportFailuresAsErrors: true lleva las expectativas fallidas al informe. Un capture con una ruta json pasa un valor a la siguiente petición.
¿Qué vigilar además de los tiempos de respuesta?
La caché, las consultas persistidas y la carga de la base de datos por petición. Cualquiera de ellas puede hacer que una prueba vaya más rápido o más lento que producción por motivos ajenos a la capacidad.
La guía de rendimiento de graphql.org señala que las consultas enviadas por GET pueden aprovechar la caché HTTP y las CDN, y que las consultas persistidas permiten al cliente enviar un hash en lugar del documento completo. Si tus clientes lo hacen, la prueba también debe hacerlo. Si no, una prueba que repite una consulta con las mismas variables mide una caché que el tráfico real no encuentra. La nota técnica de Apollo añade que Apollo Server, Gateway y el GraphOS Router activan por defecto las consultas persistidas automáticas.
La misma guía describe el problema N+1: una lista cuyo resolver carga cada elemento por separado convierte una petición en muchas consultas a la base de datos, algo que suele resolverse agrupándolas con una herramienta como DataLoader. Vigila las consultas a la base de datos por segundo junto a las peticiones por segundo; si suben mucho más rápido al crecer la carga, sospecha de un resolver. Los límites de profundidad, de complejidad y de tasa también pueden saltar durante una prueba.
¿Desde dónde debe llegar la carga?
Desde una sola máquina mientras escribes el script y haces una prueba de humo, y desde la nube cuando necesitas más carga o tráfico desde varias regiones.
LoadFocus ejecuta scripts de k6 desde 25 regiones de la nube. Allí, el número de usuarios y la duración salen de la configuración de la prueba y no de las options del script, y los escenarios con tasa de llegada funcionan con un número fijo de usuarios en lugar de una tasa fija de peticiones, la diferencia que explica el artículo sobre el modelo de carga abierto y cerrado. LoadFocus también publica una plantilla para endpoints GraphQL con consultas complejas (en inglés).
FAQ
¿Una prueba de carga de GraphQL debe enviar las consultas por GET o por POST? Como lo hagan tus clientes. graphql.org solo permite GET para operaciones de tipo query, y las mutaciones deben usar POST. Si producción envía consultas persistidas por GET a través de una CDN, prueba también ese camino.
¿El mismo script sirve para probar la carga de las suscripciones de GraphQL? Normalmente no. graphql.org explica que las suscripciones son de larga duración y suelen ir por WebSockets o server-sent events, así que necesitan su propio escenario en una herramienta que soporte ese transporte.
¿Una respuesta parcial debe contar como petición fallida? En la mayoría de las pruebas, sí, porque el cliente recibió un error en parte de lo que pidió. Si algunos errores son esperables, como resultados no encontrados para registros borrados, comprueba esos códigos de error concretos y haz fallar el resto.
Lecturas relacionadas
- Cómo hacer una prueba de carga: guía paso a paso
- Pruebas de carga en GitHub Actions: guía práctica
- Herramientas de pruebas de carga WebSocket: seis opciones
- Pruebas de carga gRPC: herramientas, configuración, métricas
Las afirmaciones técnicas de este artículo no se han revisado de forma independiente.