Módulo 2: El script de k6 y los usuarios virtuales
6. `options`: `vus` y `duration`
Descripción
Ya tienes el guion completo: en default, un VU cotiza (http.post), verifica (check) y espera (sleep). Pero el guion no dice cuántos usuarios lo representan ni por cuánto tiempo. Esa es la otra mitad de un script de k6, y vive fuera de default, en un objeto especial: export const options. Con dos campos —vus y duration— defines la escala de tu prueba: vus: 10 pone diez usuarios virtuales, duration: '30s' los mantiene corriendo treinta segundos. Es la hoja de reparto que decide si tu prueba es un ensayo con un actor o una función con cientos.
Separar el guion de la configuración es una decisión de diseño deliberada y poderosa. El mismo default —el mismo comportamiento de usuario— puede correr como un smoke test de 1 VU / 10 s o como un load test de 100 VUs / 5 min, solo cambiando options. No reescribes lo que el usuario hace; cambias cuántos y por cuánto. En esta lección abrimos options, entendemos vus y duration, y vemos la carga escalar con números reales —de 5 VUs a 10, de 20 iteraciones a 100—.
Conexión con el módulo: las lecciones 2 a 5 llenaron el guion (la función default y sus piezas); esta pone la hoja de reparto. El options de k6 se muestra como contenido; en el generador Python, vus y duration son argumentos que se ejecutan de verdad, y verás la carga crecer al subirlos. Todo lo rotulado como salida de Python fue medido en este entorno con Python 3.14.0 contra la API canónica. Aquí la carga es plana —un número fijo de VUs durante una duración fija—; hacerla variar en el tiempo (rampas, escalones, spikes con stages) es el módulo 4.
La hoja de reparto de la obra
Piénsalo así. Retoma la obra de teatro de la lección 1. El guion dice qué hace un personaje; pero antes del estreno, el director firma una hoja de reparto que decide dos cosas que el guion no toca: cuántos actores entran a escena y por cuánto tiempo dura la función. La misma obra puede montarse como una lectura íntima con un actor y veinte minutos, o como una gran producción con cien actores y tres horas —con el mismo guion, solo cambiando la hoja de reparto—.
options es esa hoja de reparto. vus es cuántos actores (usuarios virtuales) suben al escenario a la vez; duration es cuánto dura la función. El guion (default) no cambia: sigue diciendo "cotiza, verifica, espera". Lo que options decide es la escala: un ensayo con un VU o un estreno con cien. Y porque está separada del guion, puedes escalar tu prueba —de smoke a load a stress— tocando solo estos dos números, sin tocar el comportamiento del usuario.
export const optionses la hoja de reparto de tu prueba:vusdice cuántos usuarios virtuales corren a la vez ydurationpor cuánto tiempo. Vive separada del guion (default), así que la misma prueba escala de 1 VU a 100 cambiando solo dos números, sin reescribir qué hace el usuario.
options en k6 (contenido)
options se declara como una constante exportada, arriba del script, fuera de la función default. k6 la lee antes de arrancar la corrida para saber cómo montar la ejecución:
// quote_load.js — la hoja de reparto separada del guion.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
import { check, sleep } from 'k6';
// La hoja de reparto: 10 usuarios virtuales durante 30 segundos.
export const options = {
vus: 10,
duration: '30s',
};
const BASE_URL = 'http://localhost:8000';
// El guion: no cambia sin importar cuantos VUs corran.
export default function () {
const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(`${BASE_URL}/quote`, payload, params);
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
Los dos campos:
vus: 10. El número de usuarios virtuales que corren en paralelo. Diez VUs significa diez copias del guion ejecutándose a la vez, cada una en su propio bucle (lección 2). Subirvussube la concurrencia —cuánta presión simultánea recibe la API—.duration: '30s'. Cuánto tiempo corre la prueba, como una cadena con unidad:'30s'(segundos),'5m'(minutos),'1h'(horas). Durante ese tiempo, los VUs repiten el guion en bucle; cuando se cumple, k6 detiene la corrida (con una breve parada elegante para dejar terminar las iteraciones en curso) e imprime el resumen.
Con vus: 10 y duration: '30s', k6 arranca diez VUs, los deja cotizar-verificar-esperar en bucle durante treinta segundos, y cuenta todo lo que pasó. Fíjate de nuevo: el guion no sabe que hay diez VUs. Escribes el comportamiento de uno; options decide cuántos.
Por qué options va en el script (y no solo en la línea de comandos)
k6 también acepta estos valores por la línea de comandos —k6 run --vus 10 --duration 30s quote_load.js— y a veces es cómodo para una prueba rápida. Pero declarar options dentro del script tiene ventajas que lo hacen la forma preferida:
- La prueba es reproducible y versionable. El script lleva consigo su propia configuración; quien lo corra obtiene la misma carga, sin depender de recordar los flags correctos. Entra al control de versiones junto al código.
- Es autodocumentado. Abrir el archivo te dice de un vistazo qué carga impone —10 VUs, 30 s— sin buscar en un historial de comandos.
- Escala a configuraciones complejas. Cuando pases de una carga plana a perfiles con
stages, thresholds y escenarios (módulos 4, 5, 6), todo eso vive enoptions. Un solo objeto describe toda la forma de la prueba. Empezar por ponerlo en el script te prepara para eso.
La regla práctica: pon options en el script como la fuente de verdad de tu prueba; usa los flags de línea de comandos solo para experimentos puntuales o para sobrescribir un valor temporalmente. Lo que va al repositorio es el script con su options.
La carga escala, ejecutada en Python
En el generador de Python, vus y duration son argumentos —igual que los campos de options—: vus fija cuántos workers (hilos) arranca el pool, duration_s cuánto corren. Subirlos sube la carga, y podemos verlo. Corramos dos configuraciones con el mismo think time (sleep(1)), cambiando solo el reparto.
Primero, 5 VUs durante 4 segundos:
Qué esperar. Con sleep(1), cada VU hace ~1 iteración por segundo; 5 VUs × ~4 s ≈ 20 iteraciones. Salida real (5 VUs, 4 s, think 1000 ms):
----------------------------------------------------------
Generador de carga Python -> 5 VUs / 4s / think 1000ms
----------------------------------------------------------
vus............: 5
duracion real..: 4.04s
iterations.....: 20 (4.9/s)
checks.........: 100.00% (40 de 40)
status is 200....: 20 ok / 0 fail
price is correct.: 20 ok / 0 fail
http_errors....: 0
req_duration...: avg=3.09ms min=0.75ms max=7.91ms
----------------------------------------------------------
Ahora subimos el reparto: 10 VUs durante 10 segundos, mismo guion, mismo think time:
Qué esperar. 10 VUs × ~10 s ≈ 100 iteraciones, y un iterations/s cercano a 10 (el doble de VUs = el doble de ritmo). Salida real (10 VUs, 10 s, think 1000 ms):
----------------------------------------------------------
Generador de carga Python -> 10 VUs / 10s / think 1000ms
----------------------------------------------------------
vus............: 10
duracion real..: 10.21s
iterations.....: 100 (9.8/s)
checks.........: 100.00% (200 de 200)
status is 200....: 100 ok / 0 fail
price is correct.: 100 ok / 0 fail
http_errors....: 0
req_duration...: avg=5.52ms min=0.58ms max=110.15ms
----------------------------------------------------------
Compara las dos corridas, porque muestran cómo options gobierna la escala:
vus: 5 → 10. Es exactamente el campovusdeoptions, hecho argumento. Al duplicar los VUs, duplicas la concurrencia sobre la API.iterations/s: 4.9/s → 9.8/s. El ritmo total casi se duplicó, como los VUs. Consleep(1), cada VU aporta ~1 iteración/s, así que 5 VUs dan ~5/s y 10 VUs dan ~10/s. La carga escala convusde forma predecible.iterations: 20 → 100. No solo cambiaron los VUs; también la duración (4 s → 10 s). Las iteraciones totales son, a grandes rasgos,vus × duración(consleep(1)): 5 × 4 = 20, 10 × 10 = 100. Esa fórmula —el puente entre el reparto y las iteraciones— es justo la aritmética que desarma la lección 7.req_durationmax: 7.91 ms → 110.15 ms. Con más VUs golpeando a la vez, la petición más lenta creció (de 8 ms a 110 ms): la primera señal de que la concurrencia empieza a hacer trabajar a la API. Ese pico bajo carga es exactamente lo que las métricas del módulo 3 estudian a fondo (por qué el promedio se ve bien —5.52 ms— pero el peor caso cuenta otra historia). Aquí solo nota que subirvusno es gratis: la API lo siente.
vus y duration vs. iterations: dos formas de acabar
duration termina la prueba por tiempo: "corre 30 segundos, hagan las iteraciones que hagan". Es la forma más común para una carga sostenida. Pero hay una alternativa que conviene conocer:
// Terminar por numero de iteraciones, no por tiempo.
export const options = {
vus: 10,
iterations: 200, // en total, entre todos los VUs (no 200 por VU)
};
Con iterations: 200, k6 reparte 200 iteraciones entre los 10 VUs (20 cada uno) y termina cuando se completan, sin importar cuánto tarden. Es útil cuando quieres un número exacto de operaciones (por ejemplo, "procesar 200 reservas") en vez de un tiempo fijo. Un detalle importante: iterations cuenta el total repartido, no por VU —10 VUs e iterations: 200 son 20 vueltas por VU, no 200—. En este módulo usamos duration (terminar por tiempo), que es lo típico para pruebas de carga; menciono iterations para que reconozcas la opción. Lo que no debes mezclar sin querer es duration e iterations a la vez esperando que ambos manden: cada uno define un criterio de fin distinto.
Errores comunes
Poner duration sin unidad. Qué pasa: alguien escribe duration: 30 (un número) en vez de duration: '30s' (una cadena con unidad). Por qué pasa: en otros contextos las duraciones son números. Cómo detectarlo: k6 rechaza el valor o lo interpreta distinto de lo esperado; la prueba no dura lo que creías. Cómo corregirlo: duration es una cadena con unidad: '30s', '5m', '1h'. El número pelón no basta.
Confundir vus con el número de peticiones o de iteraciones. Qué pasa: alguien pone vus: 100 esperando "100 peticiones" y se desconcierta al ver miles en el resumen. Por qué pasa: vus son usuarios concurrentes, cada uno en un bucle que hace muchas iteraciones. Cómo detectarlo: las iteraciones del resumen son muchas más que los VUs. Cómo corregirlo: recuerda que vus es concurrencia, no volumen. El volumen total es, aproximadamente, vus × duración / (think + tiempo de petición). Si quieres un volumen exacto, usa iterations.
Creer que subir vus a un número enorme "prueba más" gratis. Qué pasa: alguien salta a vus: 5000 de golpe en su primera prueba. Por qué pasa: cree que más VUs es siempre mejor. Cómo detectarlo: la máquina que genera la carga (no la API) se satura —cada VU consume memoria y CPU en el generador—, y los resultados se ensucian. Cómo corregirlo: sube los VUs de forma escalonada y realista, y recuerda que el generador también tiene un límite. Además, saltar de golpe a una carga alta es justo lo que un perfil con rampa (módulo 4) evita: subir gradualmente para ver dónde empieza a doler, no solo que duele.
Ejercicios
Ejercicio 1 — Escribe la hoja de reparto. Escribe (en k6, como contenido) el options para un smoke test de 1 usuario virtual durante 30 segundos, y luego el de un load test de 50 usuarios virtuales durante 5 minutos. ¿Qué parte del script cambia entre ambos, y qué parte no?
Ver solución
Smoke test:
export const options = { vus: 1, duration: '30s' };
Load test:
export const options = { vus: 50, duration: '5m' };
Lo que cambia entre ambos es solo options (el reparto: cuántos VUs y cuánto tiempo). Lo que no cambia es la función default —el guion, lo que cada usuario hace—. Esa separación es justo la ventaja de options: la misma prueba escala de smoke a load sin tocar el comportamiento del usuario.
Ejercicio 2 — Predice las iteraciones. Con un guion que tiene sleep(1), ¿cuántas iteraciones aproximadas esperas de cada options? (a) { vus: 5, duration: '4s' }; (b) { vus: 10, duration: '10s' }; (c) { vus: 20, duration: '10s' }.
Ver solución
Con sleep(1), cada VU hace ~1 iteración por segundo, así que las iteraciones totales ≈ vus × duración:
- (a) 5 × 4 = ~20 iteraciones (coincide con la corrida real de la lección: 20).
- (b) 10 × 10 = ~100 iteraciones (coincide con la corrida real: 100).
- (c) 20 × 10 = ~200 iteraciones. El doble de VUs que en (b), el doble de iteraciones.
La fórmula funciona porque el sleep(1) fija el ritmo en ~1 vuelta por VU por segundo. Con otro think time, el ritmo cambia (lección 5), pero la idea —iteraciones crecen con vus y con duration— se mantiene.
Ejercicio 3 — duration o iterations. Para cada objetivo, di si usarías duration o iterations en options, y por qué: (a) "quiero someter la API a carga sostenida durante 10 minutos"; (b) "quiero procesar exactamente 500 reservas y ver cuánto tardan en total"; (c) "quiero un smoke test rápido de 15 segundos".
Ver solución
- (a)
duration: '10m': quieres carga por un tiempo fijo, sin importar cuántas iteraciones salgan. Es el caso típico de un load test sostenido. - (b)
iterations: 500: quieres un número exacto de operaciones (500 reservas), no un tiempo. k6 reparte las 500 entre los VUs y termina al completarlas. - (c)
duration: '15s': un smoke test se define por un tiempo corto de "¿siquiera responde bien?", así que terminar por tiempo es lo natural.
Resumen y siguiente paso
En esta lección pusiste la hoja de reparto. export const options vive fuera del guion y define la escala de la prueba con dos campos: vus (cuántos usuarios virtuales corren en paralelo) y duration (por cuánto tiempo, como cadena con unidad: '30s', '5m'). Separar la configuración del comportamiento es lo que deja escalar la misma prueba de 1 VU a 100 cambiando solo dos números. Lo ejecutaste de verdad: 5 VUs / 4 s dieron 20 iteraciones, 10 VUs / 10 s dieron 100 —la carga escala con vus y duration de forma predecible (iteraciones ≈ vus × duración con sleep(1))—, y viste la primera señal de que subir vus no es gratis: la petición más lenta pasó de 8 ms a 110 ms al duplicar la concurrencia. También conociste iterations como forma alterna de terminar (por número de operaciones, no por tiempo).
Antes de avanzar deberías poder: escribir un options con vus y duration bien formados; explicar por qué la configuración va separada del guion y por qué conviene ponerla en el script; y estimar las iteraciones de una carga plana a partir de vus, duration y el think time.
Esa fórmula que ha ido apareciendo —iteraciones ≈ vus × duración— y la confusión que la rodea son el tema de la lección 7: la diferencia entre VUs e iteraciones, y cómo leer el resumen de k6 (los bloques checks, iterations, vus) donde todos estos números aparecen juntos.
Recursos
- Opciones de k6 —
options— la referencia del objetooptionsy todos sus campos (vus,duration,iterationsy más). La fuente exacta de esta lección. - Cómo definir opciones — script vs. línea de comandos — el orden de precedencia y por qué declararlas en el script es la forma preferida. El "por qué" de poner
optionsen el archivo. - Ejecución de un test —
k6 run— cómovusydurationgobiernan la corrida, con y sin flags. Cómo se lanza la prueba que aquí es contenido. ThreadPoolExecutor(max_workers=...)— Documentación de Python — cómovusse traduce en el número de workers del pool que ejecuta la carga. El motor de las corridas medidas.