K6 Load Testing Fundamentals - Performance Testing, Stress Testing, and Building Real-World API Scenarios

K6 Load Testing Fundamentals - Performance Testing, Stress Testing, and Building Real-World API Scenarios

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.

AI Agent
AI AgentFebruary 25, 2026
0 views
18 min read

Introduction

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.

Why K6 Exists

The Performance Testing Problem

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.

The K6 Solution

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.

K6 Core Architecture

Key Concepts

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.

How K6 Works

plaintext
Test Script → K6 Engine → Virtual Users → HTTP Requests → Target API → Metrics Collection
  1. K6 reads your test script
  2. Spawns virtual users
  3. Each VU executes requests
  4. Collects metrics (latency, errors, throughput)
  5. Generates reports

K6 Execution Model

K6 uses a unique execution model optimized for performance:

plaintext
Local Machine → K6 Engine (Go-based) → Virtual Users (Lightweight)

                            HTTP Requests

                            Target API

The K6 engine is written in Go, making it extremely efficient. Each virtual user is lightweight, allowing thousands of concurrent users on a single machine.

K6 Core Concepts & Features

1. Virtual Users (VUs) and Iterations

VUs are simulated users. Each VU executes your test script independently.

Basic VU Configuration
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:

  1. Load Testing: Simulate normal traffic
  2. Stress Testing: Gradually increase load until failure
  3. Spike Testing: Sudden traffic increase
  4. Soak Testing: Sustained load over long periods

2. Stages (Ramp-Up and Ramp-Down)

Stages define how VUs scale over time.

Staged Load Testing
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:

  1. Realistic Traffic: Simulate gradual user increase
  2. Warm-up: Allow system to stabilize
  3. Cool-down: Graceful shutdown
  4. Peak Testing: Test at maximum capacity

3. Checks and Assertions

Checks validate response correctness without failing the test.

Checks and Assertions
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:

  1. Validate Responses: Ensure correct data
  2. Performance Assertions: Check response times
  3. Business Logic: Verify calculations
  4. Data Integrity: Validate returned data

4. Thresholds

Thresholds define pass/fail criteria for your test.

Thresholds
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:

  1. SLA Validation: Ensure SLA compliance
  2. Performance Gates: Prevent regressions
  3. Error Rate Limits: Acceptable failure rate
  4. Throughput Requirements: Minimum requests/second

5. Custom Metrics

Track custom metrics specific to your application.

Custom Metrics
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:

  1. Counter: Cumulative count
  2. Trend: Track values over time (min, max, avg, percentiles)
  3. Gauge: Current value
  4. Rate: Percentage of events

6. Groups

Organize related requests for better reporting.

Groups
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:

  1. Organize Tests: Group related requests
  2. Detailed Reporting: See metrics per group
  3. Debugging: Identify slow groups
  4. Business Flows: Map user journeys

7. Scenarios

Define different user behaviors and traffic patterns.

Scenarios
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:

  1. Multiple User Types: Different behaviors
  2. Concurrent Scenarios: Run simultaneously
  3. Realistic Workflows: Mix of activities
  4. Weighted Distribution: Control traffic split

8. Data Parameterization

Use external data in your tests.

Data Parameterization
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:

  1. Realistic Data: Use production-like data
  2. Multiple Users: Test with different accounts
  3. Varied Inputs: Different request payloads
  4. Distributed Load: Spread across data

9. Correlation and Dynamic Data

Extract and use data from responses.

Correlation
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:

  1. Session Management: Extract and use tokens
  2. Dynamic IDs: Use IDs from responses
  3. Realistic Flows: Simulate user journeys
  4. State Management: Maintain session state

10. Performance Optimization

K6 provides features for efficient testing.

Performance Optimization
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:

  1. Batch Requests: Reduce overhead
  2. Connection Pooling: Reuse connections
  3. Resource Efficiency: Reduce memory usage
  4. Faster Tests: Complete tests quicker

Building a Real-World E-Commerce API with NestJS

Now let's build a production-ready e-commerce API that demonstrates realistic scenarios. The system handles:

  • Product catalog with inventory
  • Shopping cart management
  • Checkout and payment processing
  • Order tracking
  • Real-time inventory updates
  • Concurrent user handling

Project Setup

Create NestJS project
npm i -g @nestjs/cli
nest new ecommerce-api
cd ecommerce-api
npm install @nestjs/typeorm typeorm pg class-validator class-transformer

Step 1: Database Configuration

src/database/database.module.ts
import { 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 {}

Step 2: Define Entities

src/entities/product.entity.ts
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;
}
src/entities/cart.entity.ts
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;
}
src/entities/cart-item.entity.ts
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;
}
src/entities/order.entity.ts
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;
}

Step 3: Products Service

src/products/products.service.ts
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);
    }
  }
}

Step 4: Cart Service

src/cart/cart.service.ts
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);
  }
}

Step 5: Orders Service

src/orders/orders.service.ts
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 } });
  }
}

Step 6: API Controllers

src/products/products.controller.ts
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 };
  }
}
src/cart/cart.controller.ts
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);
  }
}
src/orders/orders.controller.ts
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);
  }
}

Step 7: Main Application Module

src/app.module.ts
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 {}

Step 8: Docker Compose Setup

docker-compose.yml
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:

Step 9: Running the Application

Start services
# 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/seed

Real-World K6 Load Testing Scenarios

Now let's create comprehensive K6 tests that simulate realistic e-commerce scenarios.

Step 1: Basic Load Test

tests/basic-load-test.js
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);
}

Step 2: Realistic User Journey Test

tests/user-journey-test.js
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',
    });
  });
}

Step 3: Stress Test - Gradual Load Increase

tests/stress-test.js
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,
    });
  });
}

Step 4: Spike Test - Sudden Traffic Surge

tests/spike-test.js
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,
  });
}

Step 5: Soak Test - Long Duration Load

tests/soak-test.js
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);
}

Step 6: Complex Real-World Scenario

tests/complex-scenario.js
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',
    });
  });
}

Step 7: Running K6 Tests

Run K6 tests
# 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

Step 8: Analyzing Results

View results
# 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'

Common Mistakes & Pitfalls

1. Not Ramping Up Load Gradually

js
// ❌ 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 },
  ],
};

2. Ignoring Think Time

js
// ❌ 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');
}

3. Not Using Realistic Data

js
// ❌ 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,
  });
}

4. Not Checking Responses

js
// ❌ 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,
  });
}

5. Unrealistic Thresholds

js
// ❌ 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'],
  },
};

Best Practices

1. Start Small, Scale Up

Begin with small loads and gradually increase to find breaking points.

js
export const options = {
  stages: [
    { duration: '1m', target: 10 },
    { duration: '1m', target: 50 },
    { duration: '1m', target: 100 },
    { duration: '1m', target: 0 },
  ],
};

2. Use Realistic Scenarios

Simulate actual user behavior with think time and realistic workflows.

js
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);
  });
}

3. Monitor Key Metrics

Track business-relevant metrics alongside technical metrics.

js
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);
}

4. Use Thresholds for CI/CD

Define thresholds to fail tests automatically.

js
export const options = {
  thresholds: {
    http_req_failed: ['rate<0.05'],
    http_req_duration: ['p(95)<500'],
    checkout_success: ['rate>0.95'],
  },
};

5. Test Different Scenarios

Run multiple test types to understand system behavior.

bash
# 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.js

6. Correlate and Parameterize

Use dynamic data and correlation for realistic tests.

js
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}`);
}

7. Analyze Results Thoroughly

Look beyond average response times.

bash
# View percentiles
k6 run tests/load-test.js --out json=results.json
 
# Analyze
cat results.json | jq '.metrics.http_req_duration'

K6 vs Other Load Testing Tools

FeatureK6JMeterLocustArtillery
LanguageJavaScriptJava GUIPythonYAML/JS
Learning CurveEasySteepMediumEasy
ScriptingNativeLimitedNativeLimited
CI/CDExcellentGoodGoodGood
CloudYesNoNoYes
Open SourceYesYesYesYes
PerformanceExcellentGoodGoodGood

Choose K6 when:

  • Need developer-friendly scripting
  • Want CI/CD integration
  • Prefer JavaScript
  • Need cloud testing

Choose JMeter when:

  • Need GUI configuration
  • Have existing JMeter tests
  • Need advanced plugins

Choose Locust when:

  • Prefer Python
  • Need custom logic
  • Want open-source simplicity

Conclusion

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:

  • Realistic user journeys
  • Complex checkout flows
  • Inventory management under load
  • Payment processing simulation
  • Multiple concurrent scenarios
  • Business metric tracking

Key takeaways:

  1. Start with small loads and scale gradually
  2. Simulate realistic user behavior
  3. Use checks and thresholds for validation
  4. Monitor business metrics alongside technical metrics
  5. Run multiple test types (load, stress, spike, soak)
  6. Integrate tests into CI/CD pipelines
  7. Analyze results thoroughly

Next steps:

  1. Install K6 locally
  2. Write a basic load test for your API
  3. Add realistic think time and workflows
  4. Define meaningful thresholds
  5. Integrate into your CI/CD pipeline
  6. Run tests regularly to catch regressions

K6 makes performance testing accessible and practical. Master it, and you'll build systems that scale reliably under real-world traffic patterns.


Related Posts