---
title: "OAuth2 y API keys en k6: patrones de refresh de token que sobreviven la concurrencia"
description: "OAuth2 y API keys en k6 bajo concurrencia: refresh de token, client credentials, secretos por env—docs de auth HTTP k6 y patrones de carga realistas."
publishDate: "2026-07-13"
draft: false
keyword: "k6 oauth2 api key concurrencia"
intent: "MOFU"
cta: "Usa Performate para convertir este playbook en escenarios k6 ejecutables, umbrales y reportes compartibles sin perder días en código pegamento."
tags: ["k6", "pruebas-de-carga", "rendimiento-api", "oauth2"]
translationOf: k6-oauth2-api-key-concurrency
---

Tokens bearer estáticos en scripts k6 hacen que cada corrida parezca saludable—hasta que OAuth prod produce **tormentas de refresh**, **endpoints token rate-limited** y **clock skew** que generan picos 401 bajo carga. Clientes reales renuevan tokens; tu load test también debería.

OAuth2 y API keys en k6 no es «pegar el JWT de DevTools». Es modelar **ciclo de vida token bajo concurrencia**: client credentials para service accounts, refresh antes de expiración, y escenarios separados cuando API keys golpean límites RPM. En esta guía verás por qué auth colapsa bajo VUs, cómo estructurar setup y refresh en k6, y qué patrones van en smoke CI vs carga completa—no un secreto hard-coded en git.

## Por qué auth falla bajo carga—no a escala single-request

Tests funcionales obtienen un token y lo reutilizan forever. k6 concurrente expone fallos distintos:

- **Rate limits en endpoint token** throttle refresh cuando muchos VUs renuevan simultáneamente.
- **Access tokens short-lived** expiran mid-escenario—ráfagas 401 si refresh es per-iteration pero sin caché.
- **Tormentas client credentials** stampede al IdP cuando cada VU llama `/token` independientemente.
- **Caps RPM API key** devuelven `429` mientras rutas business siguen «bien» a baja tasa.
- **Clock skew** entre runners k6 y servidor auth causa errores «expired» prematuros.

Piensa tokens como credenciales de conferencia: una credencial por asistente funciona hasta que todos intentan re-imprimir en el mismo kiosk al cambio de sesión.

Referencia [OAuth 2.0 overview](https://oauth.net/2/) y [variables de entorno k6](https://k6.io/docs/using-k6/environment-variables/) para secretos. Empareja con [secretos k6 en entornos prueba](/es/blog/k6-secretos-entornos-prueba) y [gestionar env vars](/es/blog/gestionar-entornos-variables-k6).

## Implementación práctica en k6: refresh compartido con caché por VU

Usa **`setup()`** para un fetch client-credentials en smoke, o **refresh a nivel módulo con lógica tipo mutex** en corridas largas—fetch token una vez por batch de iteración VU, no una vez global forever.

**Script de ejemplo (ilustrativo—no listo para producción).** URLs IdP ficticias; adapta grant type a tu proveedor.

**Qué demuestra este ejemplo:**

- **Client credentials en setup:** un fetch token antes de escenarios cuando es aceptable para carga service-to-service.
- **Caché token por VU con chequeo TTL:** refresh cuando ventana `expires_in` está cerca—refleja comportamiento SDK.
- **Escenario separado ruta API key:** tags `auth:api_key` vs `auth:oauth` para triage.
- **Umbrales en fallos auth:** `http_req_failed{route:token}` detecta colapso IdP temprano.

```javascript
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE = __ENV.API_BASE || 'https://staging.example.com';
const TOKEN_URL = __ENV.TOKEN_URL || 'https://idp.example.com/oauth/token';

const tokenCache = {};

function getAccessToken(vuId) {
  const now = Date.now();
  const cached = tokenCache[vuId];
  if (cached && cached.expiresAt > now + 5000) {
    return cached.accessToken;
  }

  const res = http.post(
    TOKEN_URL,
    {
      grant_type: 'client_credentials',
      client_id: __ENV.CLIENT_ID,
      client_secret: __ENV.CLIENT_SECRET,
      scope: 'api.read',
    },
    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, tags: { route: 'token' } },
  );

  check(res, { 'token 200': (r) => r.status === 200 });

  const body = res.json();
  tokenCache[vuId] = {
    accessToken: body.access_token,
    expiresAt: now + (body.expires_in || 3600) * 1000,
  };
  return tokenCache[vuId].accessToken;
}

export const options = {
  scenarios: {
    oauth_api: {
      executor: 'constant-arrival-rate',
      rate: Number(__ENV.API_RPS || 30),
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 15,
      maxVUs: 60,
      exec: 'callWithOAuth',
      tags: { auth: 'oauth' },
    },
    api_key_route: {
      executor: 'constant-arrival-rate',
      rate: Number(__ENV.KEY_RPS || 10),
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 5,
      maxVUs: 20,
      exec: 'callWithApiKey',
      tags: { auth: 'api_key' },
    },
  },
  thresholds: {
    'http_req_failed{route:token}': ['rate<0.02'],
    'http_req_failed{auth:oauth}': ['rate<0.01'],
    'http_req_duration{auth:oauth}': ['p(95)<600'],
  },
};

export function callWithOAuth() {
  const token = getAccessToken(__VU);
  const res = http.get(`${BASE}/v1/orders`, {
    headers: { Authorization: `Bearer ${token}` },
    tags: { route: 'orders', auth: 'oauth' },
  });
  check(res, { 'orders 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.2);
}

export function callWithApiKey() {
  const res = http.get(`${BASE}/v1/metrics`, {
    headers: { 'X-API-Key': __ENV.API_KEY },
    tags: { route: 'metrics', auth: 'api_key' },
  });
  check(res, { 'metrics not 429': (r) => r.status !== 429 });
  sleep(0.3);
}
```

**Patrones que funcionan**

- **Escalonar refresh token:** jitter sleep antes de refresh cuando muchos VUs comparten ventanas expiry.
- **Escenario token dedicado baja tasa** para estresar IdP separado de APIs business.
- **Nunca commitear secretos**—`CLIENT_SECRET` y `API_KEY` solo vía CI/env ([guía secretos k6](/es/blog/k6-secretos-entornos-prueba)).
- **Taggear path auth** para dashboards—401 en rutas business vs ruta token difiere ([tags observabilidad k6](/es/blog/k6-observabilidad-metricas-tags)).

**Anti-patrones a evitar**

- Un token global para 500 VUs cuando proveedor emite límites per-client.
- Refresh en cada iteración sin importar TTL—crea carga IdP artificial.
- Ignorar `429` en rutas API key—producto puede golpear límites antes de saturar servidores.

**Pro tip (comando de ejemplo):**

```bash
k6 run oauth-load.js -e CLIENT_ID=$ID -e CLIENT_SECRET=$SECRET -e API_KEY=$KEY
```

**Qué demuestra este comando:** secretos inyectados en runtime—mismo patrón que CI y staging local deben usar.

## Marco de decisión: token estático vs refresh vs auth dual

| Situación | Acción recomendada |
|:---|:---|
| Smoke CI corto, token staging long-lived | Fetch setup una vez; rotar token out-of-band |
| OAuth production-like con TTL < 15m | Caché por VU + refresh antes expiry |
| API key en endpoints partner | Escenario separado; umbrales `429` |
| Rate limit IdP conocido | Cap VUs; jitter; escenario token dedicado opcional |
| Clientes mixtos OAuth + API key | Escenarios paralelos con tags `auth:*` |

**Usa token setup estático si** smoke es <2m e IdP owner aprueba token servicio compartido.

**Usa refresh por VU si** access tokens expiran durante duración test realista.

**Usa escenarios duales si** tráfico prod mezcla contexto OAuth usuario e integraciones API key.

## Observabilidad, documentación y próximos pasos

Tests carga auth fallan silenciosamente cuando 401s se tratan como errores genéricos. Antes de escalar:

- [ ] Documenta grant type, scopes, TTL token y rate limits IdP en runbook.
- [ ] Alerta separadamente en fallos `route:token` vs tasas 401 rutas business.
- [ ] Correlaciona tags k6 `auth:*` con métricas IdP durante ventanas staging.
- [ ] Automatiza smoke OAuth en CI con secret store rotado—no tokens commiteados ([pruebas de carga en CI/CD](/es/blog/pruebas-de-carga-en-ci-cd)).
- [ ] Archiva escenario y patrón inyección secretos por entorno (staging vs load lab).

## Cómo Performate simplifica load testing OAuth2

Copy-paste bearer tokens desde devtools browser no escala entre escenarios. Abajo un **ejemplo de flujo concreto** para orders (OAuth) y metrics (API key).

**Ejemplo: escenarios auth dual con secretos env**

1. **Importa colección Postman** con requests OAuth y API-key; guarda nombres env var en colección. *Problema resuelto:* rutas definidas una vez; secretos externos.
2. **Configura escenario `oauth_api` a 30 req/s** con nota pre-request enlazando token URL. *Problema resuelto:* mapa visual auth vs tráfico business.
3. **Agrega escenario `api_key_route` a 10 req/s** con header `X-API-Key` ligado a env. *Problema resuelto:* testing límite RPM sin fork script.
4. **Aplica tags `auth:oauth` y `auth:api_key`** para reportes filtrados. *Problema resuelto:* mismo modelo tags del ejemplo k6.
5. **Corre en staging con secretos desde archivo env escritorio** (nunca commiteado). *Problema resuelto:* ingeniería itera sin editar script en cada rotación token.
6. **Exporta script k6 para CI** con inyección secret store matching nombres env var local. *Problema resuelto:* ejecución federada sin drift auth.

Ese flujo mapea al `cta`: escenarios ejecutables, umbrales y reportes sin días de glue code.

## Cierre

OAuth en k6 es **problema de ciclo de vida**: refresh, rate limits y API keys necesitan escenarios y tags separados—no JWT estático del martes pasado.

Corre el load test de esta semana con refresh token habilitado—y observa si IdP o tu API golpea `429` primero al escalar VUs.

[Try Performate free](https://performate.app) | [Reserva una demo](/demo) | [Variables entorno k6](https://k6.io/docs/using-k6/environment-variables/)
