Módulo 2: El script de k6 y los usuarios virtuales
2. La función `default` y el bucle del VU
Descripción
Toda prueba de carga de k6 gira alrededor de una sola función: default. Es el guion del usuario virtual —el código que un VU ejecuta de arriba abajo, y al terminar, vuelve a ejecutar desde el principio, una y otra vez, hasta que se acaba el tiempo—. Entender esta función y el bucle que la rodea es entender el 80 % del modelo de ejecución de k6. Todo lo demás (peticiones, checks, pauses) vive dentro de esta función; y el número de usuarios y la duración viven fuera, en options. En esta lección abrimos la pieza central.
La idea que hay que fijar es contraintuitiva al principio: tú escribes lo que pasa una sola vez, y k6 lo repite. No hay un bucle for en tu código. Escribes "cotiza, verifica, espera" —una vuelta— y k6 se encarga de repetir esa vuelta en cada VU, en cada iteración, durante toda la corrida. Esa vuelta completa al guion tiene un nombre que usarás toda la guía: una iteración. Un VU que corre treinta segundos hace muchas iteraciones; contar iteraciones es contar cuántas veces se recorrió el guion completo.
Conexión con el módulo: la lección 1 te dio el mapa —guion, reparto, VUs—; esta abre la primera pieza, el guion mismo. Aquí, por primera vez en el módulo, pondremos un VU a correr de verdad: un hilo de Python que repite un guion en bucle contra la API de Reservo y cuenta sus iteraciones reales. El script de k6 se muestra como contenido; el generador Python se ejecuta. Todo lo rotulado como salida de Python fue medido en este entorno con Python 3.14.0 contra la API canónica. Las lecciones 3 a 5 llenarán el guion de contenido (peticiones, checks, pausas); aquí nos concentramos en la forma del bucle.
El torno de la fábrica
Piénsalo así. En una fábrica hay un operario en su puesto de trabajo. Su tarea está descrita en una tarjeta de instrucciones: "toma una pieza de la banda, ajústala en el torno, mécela, déjala en la caja de salida". El operario no lee "haz esto 500 veces": lee una vuelta de la tarea, la ejecuta, y cuando termina, mira la banda y empieza de nuevo con la siguiente pieza. Repite la misma tarjeta todo el turno. Si el jefe quiere más producción, no reescribe la tarjeta: pone más operarios en más puestos, todos con la misma tarjeta, trabajando en paralelo.
La función default es esa tarjeta de instrucciones: describe una vuelta del trabajo de un usuario. El VU es el operario: ejecuta la tarjeta, termina, y vuelve a empezar sin que nadie se lo diga —ese "vuelve a empezar" automático es el bucle del VU—. Y cada vuelta completa a la tarjeta es una iteración, como cada pieza terminada. Si quieres más carga, no reescribes default: pones más VUs (más operarios) en options. Tú diseñas la tarjeta; k6 gestiona el turno.
La función
defaultdescribe una sola vuelta del trabajo de un usuario. El VU la ejecuta, termina, y vuelve a empezar automáticamente: eso es el bucle del VU. Cada vuelta completa es una iteración. No escribes el bucle; escribes lo que pasa una vez y k6 lo repite.
La función default en k6 (contenido)
Este es un guion mínimo, reducido a lo esencial para ver el bucle sin distracciones. Cotiza en /quote y hace una pausa —nada más—:
// quote_loop.js — el guion minimo de un VU.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
import { sleep } from 'k6';
const BASE_URL = 'http://localhost:8000';
// Esta funcion es el guion. Un VU la ejecuta, termina, y VUELVE a ejecutarla.
export default function () {
const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
const params = { headers: { 'Content-Type': 'application/json' } };
// Una vuelta del guion = una iteracion.
http.post(`${BASE_URL}/quote`, payload, params);
sleep(1); // pausa antes de la siguiente vuelta.
}
Tres detalles clave de esta función:
export default. La palabradefaultno es un nombre cualquiera: es la exportación que k6 busca para saber qué correr. Cuando ejecutask6 run quote_loop.js, k6 toma la funcióndefaulty la convierte en el bucle de cada VU. Puedes tener otras funciones en el archivo, perodefaultes la que se repite. (Hay excepciones —setupyteardown— que veremos al final de la lección.)- No hay bucle visible. Dentro de la función hay una petición y una pausa: una vuelta. El
forque la repite no está en tu código; lo pone k6 alrededor de la función. Escribes el cuerpo de una iteración, no el bucle. - El cuerpo se ejecuta entero, en orden, cada vez. Primero la petición, luego el
sleep. Cuando termina elsleep, la iteración acaba y empieza la siguiente desde la primera línea. Ese ciclo —arriba a abajo, y otra vez— es el latido del VU.
Si corrieras este script con k6 run quote_loop.js sin opciones, k6 usaría su valor por defecto —1 VU que hace 1 iteración— y saldría. Para que el VU repita el guion durante un tiempo, necesitas decirle cuánto (con duration) o cuántas iteraciones (con iterations); eso es la lección 6. Aquí lo que importa es la forma: una función que es una vuelta, y un bucle que la envuelve.
El mismo bucle, ejecutado en Python
Ahora la cara que sí se ejecuta. Modelamos un VU con un hilo de Python que repite un guion default_fn() en un bucle while, contra la API de Reservo, hasta que se cumple una duración. Es exactamente el bucle del VU, hecho explícito:
# Un solo VU: un hilo que repite el guion default_fn() en bucle.
import json, time, urllib.request
BASE_URL = "http://127.0.0.1:PORT" # el puerto real lo asigna el SO
def default_fn():
"""El guion: una vuelta del trabajo de un usuario (una iteracion)."""
payload = json.dumps({"room": "Focus", "tier": "basic", "hours": 3}).encode()
req = urllib.request.Request(
f"{BASE_URL}/quote", data=payload,
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req) as res:
res.read()
time.sleep(1) # think time, como sleep(1) en k6
def vu_loop(duration_s):
"""El bucle del VU: repite el guion hasta cumplir la duracion."""
iterations = 0
deadline = time.perf_counter() + duration_s
while time.perf_counter() < deadline:
default_fn() # una vuelta = una iteracion
iterations += 1
return iterations
Fíjate en el mapeo, línea por línea: default_fn() es el export default function de k6; el while ... default_fn() es el bucle que k6 pone solo; iterations += 1 cuenta cada vuelta; time.sleep(1) es el sleep(1). Lo único que en k6 está oculto (el bucle) aquí está a la vista, para que lo puedas contar.
En el generador completo del módulo, este bucle corre dentro de un ThreadPoolExecutor con max_workers = número de VUs; con un solo VU, es literalmente el vu_loop de arriba corriendo en un hilo. Vamos a correrlo con 1 VU durante 5 segundos, con sleep(1) de por medio.
Qué esperar. Un VU que hace una vuelta por segundo (por el sleep(1)) debería completar alrededor de 5 iteraciones en 5 segundos. Esta es la salida real del generador en este entorno (1 VU, 5 s, think time de 1000 ms):
----------------------------------------------------------
Generador de carga Python -> 1 VUs / 5s / think 1000ms
----------------------------------------------------------
vus............: 1
duracion real..: 5.05s
iterations.....: 5 (1.0/s)
checks.........: 100.00% (10 de 10)
status is 200....: 5 ok / 0 fail
price is correct.: 5 ok / 0 fail
http_errors....: 0
req_duration...: avg=2.24ms min=0.85ms max=5.64ms
----------------------------------------------------------
Léela despacio, porque cada cifra confirma el modelo:
vus: 1— un solo usuario virtual (un hilo). Un solo operario en un puesto.iterations: 5 (1.0/s)— el guion se recorrió completo 5 veces en 5 segundos, a razón de 1 vuelta por segundo. Ese ritmo de 1/s lo dicta elsleep(1): cada iteración hace su petición (que tarda ~2 ms) y luego espera un segundo, así que una vuelta dura poco más de un segundo. Cinco segundos, cinco vueltas.req_duration avg=2.24ms— la petición HTTP en sí es rapidísima; casi todo el segundo de cada iteración es elsleep, no la petición. (Fíjate: el think time no cuenta como tiempo de petición. Esa distinción importará en el módulo 3.)
Ahí está el bucle del VU, medido: un hilo, cinco vueltas, una por segundo. No escribiste "repite 5 veces"; escribiste una vuelta y le dijiste "corre 5 segundos", y el bucle produjo las 5 iteraciones.
Qué pasa si quitas la pausa: el bucle a máxima velocidad
El sleep(1) es lo que hace que el VU vaya a "ritmo humano". ¿Y si lo quitas? El bucle sigue siendo el mismo —ejecutar el guion, volver a empezar—, pero sin la pausa, el VU golpea la API tan rápido como puede. Corramos el mismo 1 VU, ahora durante 3 segundos y sin think time:
Qué esperar. Sin pausa, la única cota es lo que tarda la petición (~0.2 ms en localhost), así que el VU debería hacer miles de iteraciones. Salida real (1 VU, 3 s, sin think time):
----------------------------------------------------------
Generador de carga Python -> 1 VUs / 3s / sin think time
----------------------------------------------------------
vus............: 1
duracion real..: 3.00s
iterations.....: 13598 (4532.5/s)
checks.........: 100.00% (27196 de 27196)
status is 200....: 13598 ok / 0 fail
price is correct.: 13598 ok / 0 fail
http_errors....: 0
req_duration...: avg=0.20ms min=0.14ms max=6.27ms
----------------------------------------------------------
El mismo VU, el mismo bucle, y 13 598 iteraciones en vez de 5. La diferencia no es el número de usuarios (sigue siendo 1) ni la duración (3 s en vez de 5); es que sin sleep cada vuelta dura solo lo que tarda la petición (~0.2 ms), no un segundo. Esto enseña dos cosas de una vez: primero, que el bucle del VU no tiene un ritmo propio —va tan rápido como se lo permitan la petición y las pausas—; y segundo, por qué el think time importa tanto (lección 5): un VU sin pausa no simula a un humano, simula a un martillo neumático. Por ahora, quédate con la idea central: el bucle repite el guion; cuántas veces lo repite depende de lo que dure cada vuelta.
El ciclo de vida: setup, default, teardown
La función default es el bucle, pero no es lo único que puede haber en un script de k6. Hay dos funciones especiales más, que se ejecutan una sola vez (no en cada iteración) y enmarcan la corrida:
// El ciclo de vida completo (mostrado como contenido).
export function setup() {
// Se ejecuta UNA vez, al principio, antes de que arranquen los VUs.
// Util para preparar datos (por ejemplo, pedir /rooms una sola vez).
return { rooms: ['Focus', 'Studio', 'Boardroom'] };
}
export default function (data) {
// Se ejecuta EN BUCLE, en cada VU, en cada iteracion.
// Recibe lo que devolvio setup() como argumento `data`.
}
export function teardown(data) {
// Se ejecuta UNA vez, al final, despues de que terminan los VUs.
// Util para limpiar (borrar reservas de prueba, cerrar recursos).
}
La analogía: si default es la tarjeta que el operario repite todo el turno, setup es el arranque de la fábrica (encender las máquinas, cargar la materia prima) que ocurre una vez antes del turno, y teardown es el cierre (apagar, limpiar) que ocurre una vez al final. Solo default está en el bucle; setup y teardown son los extremos.
En este módulo casi todo vive en default, y así lo dejamos: menciono setup/teardown para que reconozcas el ciclo de vida completo cuando lo veas, pero su uso a fondo —preparar datos compartidos, sembrar el estado— aparece cuando lo necesites en los escenarios realistas del módulo 6. Lo que importa aquí y ahora: default es la única de las tres que se repite, y ese repetir es el bucle del VU.
Errores comunes
Meter un bucle dentro de default. Qué pasa: alguien escribe for (let i = 0; i < 100; i++) { http.post(...) } dentro de default, pensando que así "genera carga". Por qué pasa: no ha interiorizado que k6 ya envuelve default en un bucle. Cómo detectarlo: cada iteración hace 100 peticiones en vez de 1, y las cuentas del resumen se disparan sin sentido. Cómo corregirlo: pon en default una sola vuelta del guion; para más carga, sube vus en options, no metas bucles. El bucle es de k6.
Creer que sleep marca el ritmo del bucle desde afuera. Qué pasa: alguien piensa que k6 corre las iteraciones a un reloj fijo (por ejemplo, "una por segundo pase lo que pase"). Por qué pasa: confunde el bucle abierto de los VUs con un temporizador. Cómo detectarlo: al quitar el sleep, las iteraciones se multiplican (como los 13 598 de arriba) y le sorprende. Cómo corregirlo: entiende que el bucle del VU es abierto: va tan rápido como dure cada vuelta. El sleep no es un reloj externo; es tiempo dentro de la vuelta que la hace durar más. Sin sleep, la vuelta dura solo la petición.
Poner lógica de preparación dentro de default. Qué pasa: alguien pide GET /rooms en cada iteración solo para "tener la lista de salas", cuando esa lista no cambia. Por qué pasa: no distingue lo que va una vez (setup) de lo que va en cada vuelta (default). Cómo detectarlo: haces peticiones repetidas a un endpoint cuyo resultado nunca cambia, inflando la carga con trabajo inútil. Cómo corregirlo: lo que se prepara una vez —cargar datos fijos, obtener un token— va en setup y se pasa a default como argumento; en default va solo el trabajo que un usuario repite de verdad.
Ejercicios
Ejercicio 1 — ¿Cuántas iteraciones? Un VU corre un guion cuyo cuerpo es una petición de ~2 ms seguida de sleep(2) (dos segundos de pausa). Si la corrida dura 10 segundos con 1 VU, ¿cuántas iteraciones aproximadas esperas, y por qué?
Ver solución
Alrededor de 5 iteraciones. Cada vuelta dura poco más de 2 segundos (2 ms de petición + 2 s de sleep ≈ 2.002 s). En 10 segundos caben unas 10 / 2 ≈ 5 vueltas. El sleep(2) domina la duración de cada iteración; la petición es despreciable frente a él. (Compáralo con el resultado real de la lección: sleep(1) daba ~1 iteración por segundo; sleep(2) da ~1 cada dos segundos.)
Ejercicio 2 — Ubica cada función en el ciclo de vida. Para cada tarea, di si va en setup, en default o en teardown: (a) cotizar una sala y verificar el precio; (b) obtener una sola vez la lista de salas para repartirla a los VUs; (c) borrar al final todas las reservas de prueba que se crearon.
Ver solución
- (a) En
default: es el trabajo que cada usuario repite en cada iteración (cotizar y verificar). - (b) En
setup: se hace una sola vez, antes de que arranquen los VUs, y su resultado se pasa adefaultcomo argumento. - (c) En
teardown: la limpieza que ocurre una sola vez, al final, después de que terminan todos los VUs.
Ejercicio 3 — Lee las dos corridas. Compara las dos salidas reales de la lección: 1 VU / 5 s / sleep(1) dio 5 iteraciones; 1 VU / 3 s / sin sleep dio 13 598. El número de VUs es el mismo (1). (a) ¿Por qué una da 5 y la otra más de 13 000? (b) ¿Qué te dice esto sobre qué gobierna el número de iteraciones?
Ver solución
- (a) Porque cambia cuánto dura cada vuelta del guion. Con
sleep(1), cada iteración dura ~1 segundo (casi todo es la pausa), así que en 5 s caben ~5. Sinsleep, cada iteración dura solo la petición (~0.2 ms), así que en 3 s caben miles. Mismo bucle, mismo VU; distinta duración por vuelta. - (b) Que el número de iteraciones no depende solo de los VUs y la duración, sino sobre todo de cuánto tarda cada iteración (petición + think time). El bucle del VU es abierto: produce tantas vueltas como quepan en el tiempo, según lo que dure cada una. Esta es la base de la aritmética VUs-vs-iteraciones de la lección 7.
Resumen y siguiente paso
En esta lección abriste el corazón del script: la función default es el guion que un VU ejecuta, y k6 la envuelve en un bucle que la repite —cada vuelta completa es una iteración—. La clave contraintuitiva: escribes una vuelta, no el bucle; para más carga se ponen más VUs, no bucles dentro de default. Lo mediste de verdad con un hilo de Python: 1 VU con sleep(1) hizo 5 iteraciones en 5 s (una por segundo), y el mismo VU sin pausa hizo 13 598 en 3 s —la prueba de que el bucle es abierto y que la duración de cada vuelta gobierna cuántas iteraciones salen—. Y viste el ciclo de vida completo: setup y teardown corren una vez en los extremos; solo default está en el bucle.
Antes de avanzar deberías poder: escribir una función default que sea una vuelta del trabajo de un usuario; explicar qué es una iteración y por qué no escribes el bucle a mano; y estimar cuántas iteraciones dará un VU según lo que dure cada vuelta.
Hasta ahora, la vuelta del guion ha sido casi vacía —una petición y una pausa—. La lección 3 la llena de sustancia: http.get y http.post con cuerpo JSON y headers, para que el VU hable de verdad con la API de Reservo —pedir /rooms, cotizar en /quote con su cuerpo {room, tier, hours}— y leamos lo que responde.
Recursos
- El modelo de ejecución de k6 — VUs e iteraciones — la referencia del VU como bucle sobre la función
defaulty de qué cuenta como una iteración. El fundamento de esta lección. - Ciclo de vida de un test de k6 —
setup,defaultyteardown: qué corre una vez y qué corre en bucle. La fuente de la sección del ciclo de vida. concurrent.futures.ThreadPoolExecutor— Documentación de Python — el pool de hilos con el que un worker por VU repite el guion. El motor de las corridas que ejecutamos.time.perf_counter— Documentación de Python — el reloj de alta resolución con el que medimos la duración y sabemos cuándo cerrar el bucle del VU.