Por Performate
Pruebas de carga GraphQL con k6: queries, batching y modos de fallo
Modela hotspots de resolvers, tormentas de POST en batch y límites de complejidad—la intuición REST engaña a equipos GraphQL.
Las pruebas de carga REST suelen martillar una URL con JSON predecible. GraphQL mueve complejidad al servidor: el mismo endpoint /graphql acepta grafos de resolvers distintos, arrays en batch y hashes de persisted queries.
Pruebas de carga GraphQL con k6 implican variar formas y profundidad de query, etiquetar nombres de operación y vigilar modos de fallo que equipos REST rara vez ven: tormentas N+1, rechazos por complejidad y amplificación de POST en batch.
En esta guía verás por qué el rendimiento GraphQL es un problema de forma de query, cómo enviar POST realistas con k6, cuándo el batching ayuda o perjudica y qué umbrales deberían frenar releases antes de que móvil envíe un nuevo bundle de persisted queries.
Por qué GraphQL rompe la intuición REST bajo carga
Un query superficial viewer { id } puede verse rápido mientras queries de producción abren listas y dataloaders:
- Grafos de resolvers multiplican idas a base de datos; RPS moderado con CPU creciente suele señalar N+1.
- Motores de costo devuelven 400 cuando profundidad o costo de campos supera política—picos de error sin saturación.
- Persisted queries desalinean cuando el gateway deshabilita documentos ad hoc—apps móviles envían hashes que staging ya no reconoce.
- Arrays HTTP batch multiplican operaciones por request; un POST se vuelve diez árboles de resolvers—eficiente y peligroso para hotspots.
- Subscriptions (si las usas) cambian el perfil de conexiones—pruebas solo HTTP no las cubren salvo que las modeles aparte.
Martillar un health-check demuestra que el servidor responde—no que listas de catálogo o checkout cumplen SLO. Combina escenarios GraphQL con análisis de cuellos de botella en API y pruebas de carga en paginación cuando connections exponen edges con cursor.
Implementación práctica en k6: operaciones, batching y tags
k6 core no trae motor GraphQL—claridad con http.post y JSON supera helpers opacos (peticiones HTTP). Etiqueta operation y query_depth (o bucket de complejidad) para separar percentiles.
Script de ejemplo (ilustrativo—no listo para producción). Endpoint y umbrales ficticios. Adapta documentos, auth y políticas de complejidad a tu gateway.
Qué demuestra este ejemplo:
- Operaciones nombradas: Tags
operation:CatalogListvsoperation:CheckoutSummarymapean fallos a áreas de producto. - Mezcla de profundidad: SharedArray rota documentos superficiales y profundos según clientes móvil + web.
- POST batch: Escenario opcional envía array de operaciones—estresa límites de batch del gateway.
- Header de complejidad: Envía
X-GraphQL-Complexitycuando el gateway puntúa queries para alinear observabilidad. - Umbrales separados: Queries profundas con p99 más laxo que lecturas superficiales—refleja SLO de producto.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
const GQL = __ENV.GQL_URL || 'https://staging.ejemplo.com/graphql';
const TOKEN = __ENV.API_TOKEN || 'reemplazar';
const documents = new SharedArray('gql_docs', function () {
return [
{
operation: 'ViewerShallow',
depth: 'shallow',
body: {
operationName: 'ViewerShallow',
query: `query ViewerShallow { viewer { id displayName } }`,
variables: {},
},
},
{
operation: 'CatalogList',
depth: 'deep',
body: {
operationName: 'CatalogList',
query: `query CatalogList($first: Int!) {
catalog(first: $first) {
edges { node { id title variants { sku price } } }
}
}`,
variables: { first: 25 },
},
},
];
});
export const options = {
scenarios: {
single_ops: {
executor: 'ramping-arrival-rate',
startRate: 5,
timeUnit: '1s',
preAllocatedVUs: 30,
maxVUs: 100,
stages: [
{ duration: '2m', target: 20 },
{ duration: '5m', target: 20 },
{ duration: '2m', target: 0 },
],
exec: 'singleOperation',
},
batch_storm: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.BATCH_RPS || 3),
timeUnit: '1s',
duration: '4m',
preAllocatedVUs: 20,
maxVUs: 60,
exec: 'batchOperations',
startTime: '9m',
},
},
thresholds: {
'http_req_duration{depth:shallow}': ['p(99)<400'],
'http_req_duration{depth:deep}': ['p(99)<1200'],
http_req_failed: ['rate<0.02'],
},
};
function gqlPost(payload, tags) {
return http.post(GQL, JSON.stringify(payload), {
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
'X-GraphQL-Complexity': 'load-test',
},
tags,
});
}
export function singleOperation() {
const doc = documents[Math.floor(Math.random() * documents.length)];
const res = gqlPost(doc.body, {
name: 'graphql',
operation: doc.operation,
depth: doc.depth,
});
check(res, {
'sin errores GraphQL': (r) => {
const json = r.json();
return !json.errors || json.errors.length === 0;
},
'data presente': (r) => r.json('data') !== null,
});
sleep(0.3);
}
export function batchOperations() {
const batchBody = documents.map((d) => d.body);
const res = gqlPost(batchBody, {
name: 'graphql_batch',
operation: 'BatchBundle',
depth: 'batch',
});
check(res, {
'batch status 200': (r) => r.status === 200,
});
sleep(0.5);
}
Rastrea puntuaciones de complejidad en logs del gateway si aplican—correlación con tags k6 en triage. Cuando introspección está deshabilitada en producción, staging debe usar la misma allowlist de persisted queries que envía móvil.
Marco de decisión: ops simples vs batch vs picos de profundidad
| Situación | Acción recomendada |
|---|---|
| Web + móvil con documentos distintos | SharedArray ponderado a conteos de operación en analytics |
| Gateway aplica complejidad | Incluir queries rechazadas en la mezcla; umbral en http_req_failed y errors GraphQL |
| BFF agrupa operaciones del cliente | Escenario batch_storm dedicado a baja RPS; comparar CPU vs baseline single-op |
| Release de persisted queries nuevas | Smoke del set de hashes bajo carga antes de envío a tiendas |
| Lecturas con paginación por connections | Añadir operaciones con cursor; combinar con guía de paginación |
Usa escenarios single-operation para SLO baseline por operación nombrada—tus dashboards principales.
Usa escenarios batch cuando clientes reales batchan—si no, pruebas un hotspot imaginario.
Usa picos de profundidad cuando marketing habilita fichas de producto más ricas—solo queries superficiales no detectan regresiones N+1.
Observabilidad y checklist pre-release
Las regresiones GraphQL se esconden en arrays errors con HTTP 200. Antes de subir RPS:
- Documentar porcentajes de mezcla por operación y qué documentos representan cada superficie cliente.
- Etiquetar k6 con
operationydepth(o bucket de complejidad) para umbrales por familia. - Fallar checks por longitud de
errors, no solo status HTTP—datos parciales rompen UX en silencio. - Comparar CPU batch vs single-op a igual RPS de negocio—no igual conteo de requests HTTP.
- Archivar versión del manifiesto de persisted queries con git SHA por run (smoke en CI/CD tras merges de SDL).
Cómo Performate itera colecciones GraphQL con disciplina de umbrales
SDL y listas de operaciones evolucionan más rápido que scripts a mano. Flujo concreto para operaciones de catálogo + checkout de este artículo:
- Importa colección GraphQL o Postman con operaciones nombradas
CatalogListyCheckoutSummary(y carpeta batch si aplica). Problema resuelto: operaciones visuales cuando cambia el SDL. - Crea escenario
single_opscon ramping arrival rate a 20 req/s y requests ponderados según analytics. Problema resuelto: forma de tráfico alineada sin reescribir executors. - Añade escenario opcional
batch_storma 3 req/s—actívalo cuando el gateway batch esté en producción. Problema resuelto: hotspots batch deliberados, no accidentales. - Tags por request:
operation:*,depth:shallow|deep|batch. Problema resuelto: informes alineados con umbrales k6 de este artículo. - Ejecuta y compara p99 superficial vs profundo antes de que móvil publique nuevo bundle persisted. Problema resuelto: un export para backend y líderes móvil.
- Exporta k6 para smoke en CI tras cambios de política del gateway.
Coincide con el cta: iterar colecciones GraphQL en staging con disciplina de umbrales.
Cierre
Las pruebas de carga GraphQL son un problema de forma de query y batching en una sola ruta HTTP. Varía documentos, etiqueta operaciones, falla en errores GraphQL y estresa arrays batch solo cuando los clientes los envían—pensar como REST en un solo endpoint omitirá el grafo de resolvers que quema CPU.
Ejecuta esta semana la mezcla de operaciones de analytics en staging antes del próximo cambio de persisted query o política de complejidad—y anota qué operación sigue poseyendo el p99 que nombra tu SLO.
Prueba Performate gratis | Reserva una demo | API JavaScript k6
¿Listo para optimizar el rendimiento de tu API?
Usa Performate para iterar colecciones GraphQL contra gateways de staging con disciplina de umbrales.