Master K6 from core concepts to production. Learn load testing, stress testing, and spike testing. Build a complete e-commerce API with NestJS and stress test with realistic scenarios including checkout flows, inventory management, and payment processing.

Every application has limits. At some point, too many users hit your API simultaneously, and everything breaks. Performance testing reveals these limits before users discover them in production.
K6 is a modern load testing platform that makes performance testing accessible, scriptable, and developer-friendly. Used by companies like Grafana, Datadog, and thousands of engineering teams, K6 enables you to simulate real-world traffic patterns and identify bottlenecks before they impact users.
In this article, we'll explore K6's architecture, understand load testing fundamentals, and build a production-ready e-commerce API with NestJS that we'll stress test with realistic scenarios including checkout flows, inventory management, and payment processing.
Traditional load testing tools had significant limitations:
Complex Setup: Tools like JMeter required GUI configuration and were difficult to version control.
Not Developer-Friendly: Non-developers couldn't easily write or modify tests.
Limited Scripting: Hard to simulate complex user behaviors and workflows.
Poor Integration: Difficult to integrate into CI/CD pipelines.
Expensive: Enterprise tools cost thousands per month.
Slow Feedback: Results took hours to analyze.
K6 was built by Grafana Labs to solve these problems:
JavaScript-Based: Write tests in JavaScript, familiar to developers.
Version Control: Tests are code, stored in Git.
CI/CD Integration: Runs in pipelines, automated performance testing.
Realistic Scenarios: Simulate complex user journeys.
Open Source: Free and community-driven.
Fast Feedback: Results in minutes, not hours.
Cloud & Local: Run tests locally or in the cloud.
Virtual Users (VUs): Simulated users executing your test script.
Iterations: Number of times each VU executes the test.
Stages: Ramp-up, steady-state, and ramp-down phases.
Metrics: Response time, throughput, error rate, custom metrics.
Thresholds: Pass/fail criteria for your tests.
Groups: Organize related requests for better reporting.
Checks: Assertions that validate responses.
Test Script → K6 Engine → Virtual Users → HTTP Requests → Target API → Metrics CollectionK6 uses a unique execution model optimized for performance:
Local Machine → K6 Engine (Go-based) → Virtual Users (Lightweight)
↓
HTTP Requests
↓
Target APIThe K6 engine is written in Go, making it extremely efficient. Each virtual user is lightweight, allowing thousands of concurrent users on a single machine.
VUs are simulated users. Each VU executes your test script independently.
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 10, // 10 virtual users
duration: '30s', // Run for 30 seconds
};
export default function () {
http.get('https://api.example.com/users');
sleep(1);
}Use Cases:
Stages define how VUs scale over time.
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 VUs
{ duration: '5m', target: 100 }, // Stay at 100 VUs
{ duration: '2m', target: 200 }, // Ramp up to 200 VUs
{ duration: '5m', target: 200 }, // Stay at 200 VUs
{ duration: '2m', target: 0 }, // Ramp down to 0 VUs
],
};
export default function () {
http.get('https://api.example.com/users');
}Use Cases:
Checks validate response correctness without failing the test.
import http from 'k6/http';
import { check } from 'k6';
export default function () {
const response = http.get('https://api.example.com/users');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'has user data': (r) => r.json('data.length') > 0,
});
}Use Cases:
Thresholds define pass/fail criteria for your test.
export const options = {
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th percentile < 500ms
http_req_failed: ['rate<0.1'], // Error rate < 10%
http_reqs: ['rate>100'], // At least 100 req/s
},
};
export default function () {
http.get('https://api.example.com/users');
}Use Cases:
Track custom metrics specific to your application.
import http from 'k6/http';
import { Counter, Trend, Gauge } from 'k6/metrics';
const checkoutTime = new Trend('checkout_duration');
const cartAdditions = new Counter('cart_additions');
const activeUsers = new Gauge('active_users');
export default function () {
const startTime = new Date();
http.post('https://api.example.com/cart/add', { productId: 1 });
cartAdditions.add(1);
const response = http.post('https://api.example.com/checkout', {
items: [{ id: 1, quantity: 1 }],
});
checkoutTime.add(new Date() - startTime);
activeUsers.add(1);
}Metric Types:
Organize related requests for better reporting.
import http from 'k6/http';
import { group } from 'k6';
export default function () {
group('User Authentication', function () {
http.post('https://api.example.com/login', {
email: 'user@example.com',
password: 'password',
});
});
group('Product Browsing', function () {
http.get('https://api.example.com/products');
http.get('https://api.example.com/products/1');
});
group('Checkout', function () {
http.post('https://api.example.com/checkout', {
items: [{ id: 1, quantity: 1 }],
});
});
}Use Cases:
Define different user behaviors and traffic patterns.
export const options = {
scenarios: {
browsing: {
executor: 'constant-vus',
vus: 50,
duration: '5m',
exec: 'browsing',
},
checkout: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 },
],
exec: 'checkout',
},
},
};
export function browsing() {
http.get('https://api.example.com/products');
}
export function checkout() {
http.post('https://api.example.com/checkout', {});
}Use Cases:
Use external data in your tests.
import http from 'k6/http';
import { check } from 'k6';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { SharedArray } from 'k6/data';
const data = new SharedArray('users', function () {
return papaparse.parse(open('./users.csv')).data;
});
export default function () {
const user = data[Math.floor(Math.random() * data.length)];
const response = http.post('https://api.example.com/login', {
email: user.email,
password: user.password,
});
check(response, {
'login successful': (r) => r.status === 200,
});
}Use Cases:
Extract and use data from responses.
import http from 'k6/http';
export default function () {
// Get product list
const productsResponse = http.get('https://api.example.com/products');
const products = productsResponse.json('data');
// Extract product ID
const productId = products[0].id;
// Use in next request
const detailsResponse = http.get(
`https://api.example.com/products/${productId}`
);
// Extract session token
const sessionToken = detailsResponse.headers['X-Session-Token'];
// Use in subsequent requests
http.post(
'https://api.example.com/cart/add',
{ productId },
{
headers: { 'X-Session-Token': sessionToken },
}
);
}Use Cases:
K6 provides features for efficient testing.
import http from 'k6/http';
import { batch } from 'k6/http';
export const options = {
// Connection pooling
ext: {
loadimpact: {
projectID: 3356643,
name: 'API Load Test',
},
},
};
export default function () {
// Batch requests for efficiency
batch([
['GET', 'https://api.example.com/users'],
['GET', 'https://api.example.com/products'],
['GET', 'https://api.example.com/orders'],
]);
}Use Cases:
Now let's build a production-ready e-commerce API that demonstrates realistic scenarios. The system handles:
npm i -g @nestjs/cli
nest new ecommerce-api
cd ecommerce-api
npm install @nestjs/typeorm typeorm pg class-validator class-transformerimport { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'ecommerce',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: true,
}),
],
})
export class DatabaseModule {}import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { CartItem } from './cart-item.entity';
@Entity('products')
export class Product {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column('text')
description: string;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
@Column({ default: 0 })
stock: number;
@Column({ default: 0 })
reserved: number;
@Column({ default: true })
isActive: boolean;
@OneToMany(() => CartItem, (item) => item.product)
cartItems: CartItem[];
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;
}import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { CartItem } from './cart-item.entity';
@Entity('carts')
export class Cart {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
sessionId: string;
@OneToMany(() => CartItem, (item) => item.cart, { cascade: true })
items: CartItem[];
@Column('decimal', { precision: 10, scale: 2, default: 0 })
totalPrice: number;
@Column({ default: 0 })
itemCount: number;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;
}import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { Cart } from './cart.entity';
import { Product } from './product.entity';
@Entity('cart_items')
export class CartItem {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Cart, (cart) => cart.items, { onDelete: 'CASCADE' })
cart: Cart;
@ManyToOne(() => Product, (product) => product.cartItems)
product: Product;
@Column()
quantity: number;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
}import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
sessionId: string;
@Column('jsonb')
items: Array<{ productId: number; quantity: number; price: number }>;
@Column('decimal', { precision: 10, scale: 2 })
totalAmount: number;
@Column({ default: 'pending', enum: ['pending', 'processing', 'completed', 'failed'] })
status: string;
@Column({ nullable: true })
paymentId: string;
@Column({ nullable: true })
paymentStatus: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;
}import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from '../entities/product.entity';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private productRepository: Repository<Product>,
) {}
async getAllProducts(limit: number = 20, offset: number = 0) {
return this.productRepository.find({
where: { isActive: true },
take: limit,
skip: offset,
});
}
async getProductById(id: number) {
return this.productRepository.findOne({ where: { id } });
}
async getProductsByCategory(category: string) {
return this.productRepository.find({
where: { isActive: true },
take: 20,
});
}
async checkAvailability(productId: number, quantity: number) {
const product = await this.productRepository.findOne({
where: { id: productId },
});
if (!product) return false;
return product.stock - product.reserved >= quantity;
}
async reserveStock(productId: number, quantity: number) {
const product = await this.productRepository.findOne({
where: { id: productId },
});
if (!product || product.stock - product.reserved < quantity) {
throw new Error('Insufficient stock');
}
product.reserved += quantity;
return this.productRepository.save(product);
}
async releaseStock(productId: number, quantity: number) {
const product = await this.productRepository.findOne({
where: { id: productId },
});
if (product) {
product.reserved = Math.max(0, product.reserved - quantity);
await this.productRepository.save(product);
}
}
async confirmStock(productId: number, quantity: number) {
const product = await this.productRepository.findOne({
where: { id: productId },
});
if (product) {
product.stock -= quantity;
product.reserved = Math.max(0, product.reserved - quantity);
await this.productRepository.save(product);
}
}
}import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cart } from '../entities/cart.entity';
import { CartItem } from '../entities/cart-item.entity';
import { ProductsService } from '../products/products.service';
@Injectable()
export class CartService {
constructor(
@InjectRepository(Cart)
private cartRepository: Repository<Cart>,
@InjectRepository(CartItem)
private cartItemRepository: Repository<CartItem>,
private productsService: ProductsService,
) {}
async getOrCreateCart(sessionId: string) {
let cart = await this.cartRepository.findOne({
where: { sessionId },
relations: ['items', 'items.product'],
});
if (!cart) {
cart = this.cartRepository.create({ sessionId });
await this.cartRepository.save(cart);
}
return cart;
}
async addToCart(sessionId: string, productId: number, quantity: number) {
const cart = await this.getOrCreateCart(sessionId);
const product = await this.productsService.getProductById(productId);
if (!product) throw new Error('Product not found');
const available = await this.productsService.checkAvailability(
productId,
quantity,
);
if (!available) throw new Error('Insufficient stock');
let cartItem = await this.cartItemRepository.findOne({
where: { cart: { id: cart.id }, product: { id: productId } },
});
if (cartItem) {
cartItem.quantity += quantity;
} else {
cartItem = this.cartItemRepository.create({
cart,
product,
quantity,
price: product.price,
});
}
await this.cartItemRepository.save(cartItem);
await this.updateCartTotals(cart.id);
return cart;
}
async removeFromCart(sessionId: string, productId: number) {
const cart = await this.getOrCreateCart(sessionId);
await this.cartItemRepository.delete({
cart: { id: cart.id },
product: { id: productId },
});
await this.updateCartTotals(cart.id);
return cart;
}
async updateCartTotals(cartId: string) {
const cart = await this.cartRepository.findOne({
where: { id: cartId },
relations: ['items'],
});
let totalPrice = 0;
let itemCount = 0;
for (const item of cart.items) {
totalPrice += item.price * item.quantity;
itemCount += item.quantity;
}
cart.totalPrice = totalPrice;
cart.itemCount = itemCount;
return this.cartRepository.save(cart);
}
async clearCart(sessionId: string) {
const cart = await this.getOrCreateCart(sessionId);
await this.cartItemRepository.delete({ cart: { id: cart.id } });
cart.totalPrice = 0;
cart.itemCount = 0;
return this.cartRepository.save(cart);
}
}import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';
import { CartService } from '../cart/cart.service';
import { ProductsService } from '../products/products.service';
@Injectable()
export class OrdersService {
constructor(
@InjectRepository(Order)
private orderRepository: Repository<Order>,
private cartService: CartService,
private productsService: ProductsService,
) {}
async createOrder(sessionId: string) {
const cart = await this.cartService.getOrCreateCart(sessionId);
if (cart.items.length === 0) {
throw new Error('Cart is empty');
}
// Reserve stock for all items
for (const item of cart.items) {
await this.productsService.reserveStock(item.product.id, item.quantity);
}
const order = this.orderRepository.create({
sessionId,
items: cart.items.map((item) => ({
productId: item.product.id,
quantity: item.quantity,
price: item.price,
})),
totalAmount: cart.totalPrice,
status: 'pending',
});
return this.orderRepository.save(order);
}
async processPayment(orderId: string, paymentDetails: any) {
const order = await this.orderRepository.findOne({
where: { id: orderId },
});
if (!order) throw new Error('Order not found');
// Simulate payment processing
const paymentId = `PAY-${Date.now()}`;
const isSuccessful = Math.random() > 0.05; // 95% success rate
order.paymentId = paymentId;
order.paymentStatus = isSuccessful ? 'completed' : 'failed';
order.status = isSuccessful ? 'processing' : 'failed';
if (!isSuccessful) {
// Release reserved stock on payment failure
for (const item of order.items) {
await this.productsService.releaseStock(item.productId, item.quantity);
}
}
return this.orderRepository.save(order);
}
async completeOrder(orderId: string) {
const order = await this.orderRepository.findOne({
where: { id: orderId },
});
if (!order) throw new Error('Order not found');
// Confirm stock deduction
for (const item of order.items) {
await this.productsService.confirmStock(item.productId, item.quantity);
}
order.status = 'completed';
return this.orderRepository.save(order);
}
async getOrderStatus(orderId: string) {
return this.orderRepository.findOne({ where: { id: orderId } });
}
}import { Controller, Get, Param, Query } from '@nestjs/common';
import { ProductsService } from './products.service';
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Get()
async getAllProducts(
@Query('limit') limit: number = 20,
@Query('offset') offset: number = 0,
) {
const products = await this.productsService.getAllProducts(limit, offset);
return { count: products.length, products };
}
@Get(':id')
async getProduct(@Param('id') id: number) {
const product = await this.productsService.getProductById(id);
return product;
}
@Get(':id/availability')
async checkAvailability(
@Param('id') id: number,
@Query('quantity') quantity: number,
) {
const available = await this.productsService.checkAvailability(id, quantity);
return { productId: id, quantity, available };
}
}import { Controller, Post, Get, Delete, Body, Headers } from '@nestjs/common';
import { CartService } from './cart.service';
@Controller('cart')
export class CartController {
constructor(private readonly cartService: CartService) {}
@Get()
async getCart(@Headers('x-session-id') sessionId: string) {
return this.cartService.getOrCreateCart(sessionId);
}
@Post('add')
async addToCart(
@Headers('x-session-id') sessionId: string,
@Body() addItemDto: { productId: number; quantity: number },
) {
return this.cartService.addToCart(
sessionId,
addItemDto.productId,
addItemDto.quantity,
);
}
@Delete('remove/:productId')
async removeFromCart(
@Headers('x-session-id') sessionId: string,
@Param('productId') productId: number,
) {
return this.cartService.removeFromCart(sessionId, productId);
}
@Delete('clear')
async clearCart(@Headers('x-session-id') sessionId: string) {
return this.cartService.clearCart(sessionId);
}
}import { Controller, Post, Get, Body, Param, Headers } from '@nestjs/common';
import { OrdersService } from './orders.service';
@Controller('orders')
export class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Post('create')
async createOrder(@Headers('x-session-id') sessionId: string) {
return this.ordersService.createOrder(sessionId);
}
@Post(':id/payment')
async processPayment(
@Param('id') orderId: string,
@Body() paymentDetails: any,
) {
return this.ordersService.processPayment(orderId, paymentDetails);
}
@Post(':id/complete')
async completeOrder(@Param('id') orderId: string) {
return this.ordersService.completeOrder(orderId);
}
@Get(':id')
async getOrderStatus(@Param('id') orderId: string) {
return this.ordersService.getOrderStatus(orderId);
}
}import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseModule } from './database/database.module';
import { Product } from './entities/product.entity';
import { Cart } from './entities/cart.entity';
import { CartItem } from './entities/cart-item.entity';
import { Order } from './entities/order.entity';
import { ProductsService } from './products/products.service';
import { ProductsController } from './products/products.controller';
import { CartService } from './cart/cart.service';
import { CartController } from './cart/cart.controller';
import { OrdersService } from './orders/orders.service';
import { OrdersController } from './orders/orders.controller';
@Module({
imports: [
DatabaseModule,
TypeOrmModule.forFeature([Product, Cart, CartItem, Order]),
],
controllers: [ProductsController, CartController, OrdersController],
providers: [ProductsService, CartService, OrdersService],
})
export class AppModule {}version: '3.8'
services:
postgres:
image: postgres:15
ports:
- '5432:5432'
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: ecommerce
volumes:
- postgres_data:/var/lib/postgresql/data
api:
build: .
ports:
- '3000:3000'
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: password
DB_NAME: ecommerce
depends_on:
- postgres
volumes:
postgres_data:# Start services
docker-compose up -d
# Install dependencies
npm install
# Run application
npm run start:dev
# Seed database with products
curl -X POST http://localhost:3000/admin/seedNow let's create comprehensive K6 tests that simulate realistic e-commerce scenarios.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 VUs
{ duration: '5m', target: 100 }, // Stay at 100 VUs
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.1'],
},
};
export default function () {
const baseUrl = 'http://localhost:3000';
const sessionId = `session-${__VU}-${__ITER}`;
// Browse products
const productsRes = http.get(`${baseUrl}/products?limit=20`, {
headers: { 'x-session-id': sessionId },
});
check(productsRes, {
'products list status 200': (r) => r.status === 200,
'products list response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}import http from 'k6/http';
import { check, group, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 100 },
{ duration: '2m', target: 150 },
{ duration: '3m', target: 150 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.05'],
'group_duration{group:::browse}': ['p(95)<300'],
'group_duration{group:::checkout}': ['p(95)<1000'],
},
};
export default function () {
const baseUrl = 'http://localhost:3000';
const sessionId = `session-${__VU}-${Date.now()}`;
group('Browse Products', function () {
const productsRes = http.get(`${baseUrl}/products?limit=20`, {
headers: { 'x-session-id': sessionId },
});
check(productsRes, {
'browse status 200': (r) => r.status === 200,
'browse response time < 300ms': (r) => r.timings.duration < 300,
});
sleep(2);
});
group('View Product Details', function () {
const detailRes = http.get(`${baseUrl}/products/1`, {
headers: { 'x-session-id': sessionId },
});
check(detailRes, {
'detail status 200': (r) => r.status === 200,
});
sleep(1);
});
group('Add to Cart', function () {
const addRes = http.post(
`${baseUrl}/cart/add`,
JSON.stringify({
productId: 1,
quantity: 2,
}),
{
headers: {
'x-session-id': sessionId,
'Content-Type': 'application/json',
},
}
);
check(addRes, {
'add to cart status 200': (r) => r.status === 200,
'cart has items': (r) => r.json('itemCount') > 0,
});
sleep(1);
});
group('Checkout', function () {
const createOrderRes = http.post(
`${baseUrl}/orders/create`,
null,
{
headers: { 'x-session-id': sessionId },
}
);
check(createOrderRes, {
'create order status 200': (r) => r.status === 200,
'order has ID': (r) => r.json('id') !== undefined,
});
const orderId = createOrderRes.json('id');
sleep(1);
const paymentRes = http.post(
`${baseUrl}/orders/${orderId}/payment`,
JSON.stringify({
method: 'credit_card',
amount: createOrderRes.json('totalAmount'),
}),
{
headers: { 'Content-Type': 'application/json' },
}
);
check(paymentRes, {
'payment status 200': (r) => r.status === 200,
'payment processed': (r) => r.json('paymentStatus') !== undefined,
});
sleep(1);
const completeRes = http.post(
`${baseUrl}/orders/${orderId}/complete`,
null
);
check(completeRes, {
'complete order status 200': (r) => r.status === 200,
'order completed': (r) => r.json('status') === 'completed',
});
});
}import http from 'k6/http';
import { check, group } from 'k6';
import { Counter, Trend } from 'k6/metrics';
const checkoutErrors = new Counter('checkout_errors');
const checkoutTime = new Trend('checkout_duration');
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '2m', target: 300 },
{ duration: '2m', target: 400 },
{ duration: '2m', target: 500 },
{ duration: '3m', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.1'],
checkout_errors: ['count<50'],
checkout_duration: ['p(95)<2000'],
},
};
export default function () {
const baseUrl = 'http://localhost:3000';
const sessionId = `stress-${__VU}-${Date.now()}`;
group('Stress Test - Full Checkout Flow', function () {
const startTime = new Date();
// Browse
http.get(`${baseUrl}/products?limit=20`, {
headers: { 'x-session-id': sessionId },
});
// Add to cart
const addRes = http.post(
`${baseUrl}/cart/add`,
JSON.stringify({ productId: Math.floor(Math.random() * 100) + 1, quantity: 1 }),
{
headers: {
'x-session-id': sessionId,
'Content-Type': 'application/json',
},
}
);
// Create order
const orderRes = http.post(`${baseUrl}/orders/create`, null, {
headers: { 'x-session-id': sessionId },
});
if (orderRes.status !== 200) {
checkoutErrors.add(1);
}
const orderId = orderRes.json('id');
// Process payment
const paymentRes = http.post(
`${baseUrl}/orders/${orderId}/payment`,
JSON.stringify({ method: 'credit_card' }),
{ headers: { 'Content-Type': 'application/json' } }
);
// Complete order
const completeRes = http.post(`${baseUrl}/orders/${orderId}/complete`, null);
const duration = new Date() - startTime;
checkoutTime.add(duration);
check(completeRes, {
'checkout successful': (r) => r.status === 200,
});
});
}import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Normal load
{ duration: '30s', target: 500 }, // Spike to 500 VUs
{ duration: '1m', target: 500 }, // Maintain spike
{ duration: '30s', target: 50 }, // Back to normal
{ duration: '1m', target: 0 }, // Cool down
],
thresholds: {
http_req_duration: ['p(95)<1000'],
http_req_failed: ['rate<0.2'],
},
};
export default function () {
const baseUrl = 'http://localhost:3000';
const sessionId = `spike-${__VU}-${Date.now()}`;
const res = http.get(`${baseUrl}/products?limit=20`, {
headers: { 'x-session-id': sessionId },
});
check(res, {
'spike test status 200': (r) => r.status === 200,
'spike test response time < 1s': (r) => r.timings.duration < 1000,
});
}import http from 'k6/http';
import { check } from 'k6';
import { Gauge } from 'k6/metrics';
const memoryUsage = new Gauge('memory_usage');
export const options = {
stages: [
{ duration: '5m', target: 100 }, // Ramp up
{ duration: '30m', target: 100 }, // Soak for 30 minutes
{ duration: '5m', target: 0 }, // Ramp down
],
thresholds: {
http_req_failed: ['rate<0.05'],
http_req_duration: ['p(99)<1000'],
},
};
export default function () {
const baseUrl = 'http://localhost:3000';
const sessionId = `soak-${__VU}-${Date.now()}`;
// Simulate realistic user behavior
const res = http.get(`${baseUrl}/products?limit=20`, {
headers: { 'x-session-id': sessionId },
});
check(res, {
'soak test status 200': (r) => r.status === 200,
});
// Track memory usage
memoryUsage.add(Math.random() * 100);
}import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Counter, Trend, Rate } from 'k6/metrics';
const cartAbandonmentRate = new Rate('cart_abandonment');
const checkoutSuccessRate = new Rate('checkout_success');
const inventoryErrors = new Counter('inventory_errors');
const paymentFailures = new Counter('payment_failures');
const checkoutDuration = new Trend('checkout_duration');
export const options = {
scenarios: {
browsers: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 },
],
exec: 'browsing',
},
buyers: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
exec: 'checkout',
},
},
thresholds: {
http_req_failed: ['rate<0.1'],
checkout_success: ['rate>0.9'],
checkout_duration: ['p(95)<2000'],
},
};
export function browsing() {
const baseUrl = 'http://localhost:3000';
const sessionId = `browser-${__VU}-${Date.now()}`;
group('Product Browsing', function () {
// List products
const listRes = http.get(`${baseUrl}/products?limit=20&offset=0`, {
headers: { 'x-session-id': sessionId },
});
check(listRes, {
'list status 200': (r) => r.status === 200,
});
sleep(2);
// View random product
const productId = Math.floor(Math.random() * 100) + 1;
const detailRes = http.get(`${baseUrl}/products/${productId}`, {
headers: { 'x-session-id': sessionId },
});
check(detailRes, {
'detail status 200': (r) => r.status === 200,
});
sleep(3);
// Check availability
const availRes = http.get(
`${baseUrl}/products/${productId}/availability?quantity=1`,
{
headers: { 'x-session-id': sessionId },
}
);
check(availRes, {
'availability status 200': (r) => r.status === 200,
});
sleep(1);
});
}
export function checkout() {
const baseUrl = 'http://localhost:3000';
const sessionId = `buyer-${__VU}-${Date.now()}`;
const startTime = new Date();
group('Complete Checkout Flow', function () {
// Browse products
http.get(`${baseUrl}/products?limit=20`, {
headers: { 'x-session-id': sessionId },
});
sleep(1);
// Add multiple items to cart
const productIds = [1, 2, 3];
let cartEmpty = false;
for (const productId of productIds) {
const addRes = http.post(
`${baseUrl}/cart/add`,
JSON.stringify({
productId,
quantity: Math.floor(Math.random() * 3) + 1,
}),
{
headers: {
'x-session-id': sessionId,
'Content-Type': 'application/json',
},
}
);
if (addRes.status !== 200) {
inventoryErrors.add(1);
cartEmpty = true;
break;
}
sleep(0.5);
}
cartAbandonmentRate.add(cartEmpty ? 1 : 0);
if (cartEmpty) {
return; // Abandon cart
}
// Get cart
const cartRes = http.get(`${baseUrl}/cart`, {
headers: { 'x-session-id': sessionId },
});
sleep(1);
// Create order
const orderRes = http.post(`${baseUrl}/orders/create`, null, {
headers: { 'x-session-id': sessionId },
});
if (orderRes.status !== 200) {
checkoutSuccessRate.add(0);
return;
}
const orderId = orderRes.json('id');
sleep(1);
// Process payment
const paymentRes = http.post(
`${baseUrl}/orders/${orderId}/payment`,
JSON.stringify({
method: 'credit_card',
amount: orderRes.json('totalAmount'),
}),
{
headers: { 'Content-Type': 'application/json' },
}
);
if (paymentRes.status !== 200 || paymentRes.json('paymentStatus') === 'failed') {
paymentFailures.add(1);
checkoutSuccessRate.add(0);
return;
}
sleep(1);
// Complete order
const completeRes = http.post(`${baseUrl}/orders/${orderId}/complete`, null);
const duration = new Date() - startTime;
checkoutDuration.add(duration);
checkoutSuccessRate.add(completeRes.status === 200 ? 1 : 0);
check(completeRes, {
'order completed': (r) => r.status === 200,
'order status is completed': (r) => r.json('status') === 'completed',
});
});
}# Install K6
# macOS
brew install k6
# Linux
sudo apt-get install k6
# Windows
choco install k6
# Run basic load test
k6 run tests/basic-load-test.js
# Run with output
k6 run tests/user-journey-test.js --out json=results.json
# Run stress test
k6 run tests/stress-test.js
# Run spike test
k6 run tests/spike-test.js
# Run soak test
k6 run tests/soak-test.js
# Run complex scenario
k6 run tests/complex-scenario.js
# Run with custom options
k6 run tests/user-journey-test.js --vus 50 --duration 5m
# Run with cloud
k6 cloud tests/user-journey-test.js# Generate HTML report
k6 run tests/user-journey-test.js --out json=results.json
# View summary
cat results.json | jq '.metrics'
# Filter specific metrics
cat results.json | jq '.metrics | keys'// ❌ Wrong - sudden spike
export const options = {
vus: 1000,
duration: '1m',
};
// ✅ Correct - gradual ramp-up
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '2m', target: 500 },
{ duration: '2m', target: 1000 },
{ duration: '2m', target: 0 },
],
};// ❌ Wrong - no delays between requests
export default function () {
http.get('https://api.example.com/products');
http.get('https://api.example.com/products/1');
http.get('https://api.example.com/cart/add');
}
// ✅ Correct - realistic think time
export default function () {
http.get('https://api.example.com/products');
sleep(2);
http.get('https://api.example.com/products/1');
sleep(1);
http.get('https://api.example.com/cart/add');
}// ❌ Wrong - same data every time
export default function () {
http.post('https://api.example.com/orders', {
productId: 1,
quantity: 1,
});
}
// ✅ Correct - varied data
export default function () {
http.post('https://api.example.com/orders', {
productId: Math.floor(Math.random() * 1000) + 1,
quantity: Math.floor(Math.random() * 10) + 1,
});
}// ❌ Wrong - no validation
export default function () {
http.get('https://api.example.com/users');
}
// ✅ Correct - validate responses
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
}// ❌ Wrong - impossible thresholds
export const options = {
thresholds: {
http_req_duration: ['p(99)<10'], // 99th percentile < 10ms
http_req_failed: ['rate<0.001'], // Less than 0.1% errors
},
};
// ✅ Correct - realistic thresholds
export const options = {
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.05'],
},
};Begin with small loads and gradually increase to find breaking points.
export const options = {
stages: [
{ duration: '1m', target: 10 },
{ duration: '1m', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '1m', target: 0 },
],
};Simulate actual user behavior with think time and realistic workflows.
export default function () {
group('Browse', () => {
http.get('https://api.example.com/products');
sleep(2);
});
group('View Details', () => {
http.get('https://api.example.com/products/1');
sleep(1);
});
group('Checkout', () => {
http.post('https://api.example.com/checkout', {});
sleep(1);
});
}Track business-relevant metrics alongside technical metrics.
const checkoutSuccess = new Rate('checkout_success');
const cartAbandonmentRate = new Rate('cart_abandonment');
const paymentFailures = new Counter('payment_failures');
export default function () {
// Track metrics
checkoutSuccess.add(success ? 1 : 0);
cartAbandonmentRate.add(abandoned ? 1 : 0);
paymentFailures.add(failed ? 1 : 0);
}Define thresholds to fail tests automatically.
export const options = {
thresholds: {
http_req_failed: ['rate<0.05'],
http_req_duration: ['p(95)<500'],
checkout_success: ['rate>0.95'],
},
};Run multiple test types to understand system behavior.
# Load test
k6 run tests/load-test.js
# Stress test
k6 run tests/stress-test.js
# Spike test
k6 run tests/spike-test.js
# Soak test
k6 run tests/soak-test.jsUse dynamic data and correlation for realistic tests.
export default function () {
const productsRes = http.get('https://api.example.com/products');
const productId = productsRes.json('data.0.id');
http.get(`https://api.example.com/products/${productId}`);
}Look beyond average response times.
# View percentiles
k6 run tests/load-test.js --out json=results.json
# Analyze
cat results.json | jq '.metrics.http_req_duration'| Feature | K6 | JMeter | Locust | Artillery |
|---|---|---|---|---|
| Language | JavaScript | Java GUI | Python | YAML/JS |
| Learning Curve | Easy | Steep | Medium | Easy |
| Scripting | Native | Limited | Native | Limited |
| CI/CD | Excellent | Good | Good | Good |
| Cloud | Yes | No | No | Yes |
| Open Source | Yes | Yes | Yes | Yes |
| Performance | Excellent | Good | Good | Good |
Choose K6 when:
Choose JMeter when:
Choose Locust when:
K6 transforms performance testing from a specialized skill to a developer practice. Understanding load testing fundamentals—VUs, stages, checks, and thresholds—enables you to build reliable systems.
The e-commerce platform example demonstrates production patterns:
Key takeaways:
Next steps:
K6 makes performance testing accessible and practical. Master it, and you'll build systems that scale reliably under real-world traffic patterns.