Implementing Grafana k6 Load Testing in GRIT Framework
A comprehensive guide to setting up and running performance tests with Grafana k6 in a social media management platform
Implementing Grafana k6 Load Testing in GRIT Framework
![]()
Introduction
Performance testing is crucial for any production application, especially for platforms handling concurrent users and real-time operations. In this guide, I'll walk you through implementing Grafana k6 load testing in the GRIT social media management framework - a full-stack application built with Go, Next.js, PostgreSQL, and Redis.
What is Grafana k6?
k6 is an open-source load testing tool built for developers. It allows you to write test scripts in JavaScript and provides detailed performance metrics. Unlike traditional load testing tools, k6 is:
- Developer-friendly: Write tests in JavaScript
- CLI-first: Perfect for CI/CD integration
- Performant: Written in Go, handles thousands of VUs (Virtual Users)
- Cloud-ready: Integrates with Grafana Cloud for advanced analytics
Prerequisites
Before we begin, ensure you have:
- Docker and Docker Compose installed
- Node.js and pnpm (for the frontend)
- Go 1.21+ (for the backend)
- Basic understanding of HTTP APIs and JavaScript
Project Architecture Overview
Our GRIT framework consists of:
social-media-manager/
├── apps/
│ ├── api/ # Go backend (Fiber framework)
│ ├── admin/ # Next.js admin dashboard
│ └── web/ # Next.js public website
├── k6/ # Load testing scripts
├── docker-compose.yml
└── .env
Step 1: Install Grafana k6
macOS (Homebrew)
brew install k6Linux (Debian/Ubuntu)
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6Windows (Chocolatey)
choco install k6Docker (Alternative)
docker pull grafana/k6:latestVerify installation:
k6 versionStep 2: Project Setup
1. Create k6 Directory Structure
mkdir -p k6
cd k62. Create Base Configuration
Create k6/config.js:
export const BASE_URL = __ENV.BASE_URL || "http://localhost:8080"
export const FRONTEND_URL = __ENV.FRONTEND_URL || "http://localhost:3000"
export const defaultOptions = {
stages: [
{ duration: "30s", target: 10 }, // Ramp up to 10 users
{ duration: "1m", target: 10 }, // Stay at 10 users
{ duration: "30s", target: 0 }, // Ramp down to 0 users
],
thresholds: {
http_req_duration: ["p(95)<500"], // 95% of requests under 500ms
http_req_failed: ["rate<0.01"], // Less than 1% errors
},
}Step 3: Create Authentication Test
Create k6/auth-test.js:
import http from "k6/http"
import { check, sleep } from "k6"
import { Rate } from "k6/metrics"
import { BASE_URL, defaultOptions } from "./config.js"
export const options = defaultOptions
const errorRate = new Rate("errors")
export function setup() {
// Create test user
const signupPayload = JSON.stringify({
email: `test-${Date.now()}@example.com`,
password: "Test123!@#",
name: "Test User",
})
const signupRes = http.post(`${BASE_URL}/api/auth/signup`, signupPayload, {
headers: { "Content-Type": "application/json" },
})
check(signupRes, {
"signup successful": (r) => r.status === 201,
})
return {
email: JSON.parse(signupPayload).email,
password: "Test123!@#",
}
}
export default function (data) {
// Login
const loginPayload = JSON.stringify({
email: data.email,
password: data.password,
})
const loginRes = http.post(`${BASE_URL}/api/auth/login`, loginPayload, {
headers: { "Content-Type": "application/json" },
})
const loginSuccess = check(loginRes, {
"login status is 200": (r) => r.status === 200,
"has access token": (r) => r.json("access_token") !== undefined,
})
errorRate.add(!loginSuccess)
if (loginSuccess) {
const token = loginRes.json("access_token")
// Get user profile
const profileRes = http.get(`${BASE_URL}/api/auth/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
check(profileRes, {
"profile status is 200": (r) => r.status === 200,
"has user data": (r) => r.json("email") !== undefined,
})
}
sleep(1)
}
export function teardown(data) {
console.log("Test completed")
}Step 4: Create API Load Test
Create k6/api-test.js:
import http from "k6/http"
import { check, group, sleep } from "k6"
import { Counter, Trend } from "k6/metrics"
import { BASE_URL } from "./config.js"
export const options = {
stages: [
{ duration: "1m", target: 50 }, // Ramp up to 50 users
{ duration: "3m", target: 50 }, // Stay at 50 users
{ duration: "1m", target: 100 }, // Spike to 100 users
{ duration: "2m", target: 100 }, // Stay at 100 users
{ duration: "1m", target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ["p(95)<1000", "p(99)<2000"],
http_req_failed: ["rate<0.05"],
"group_duration{group:::API Health}": ["avg<200"],
"group_duration{group:::Posts API}": ["avg<500"],
},
}
const apiErrors = new Counter("api_errors")
const apiDuration = new Trend("api_duration")
let authToken
export function setup() {
// Authenticate once for all VUs
const loginPayload = JSON.stringify({
email: "admin@example.com",
password: "admin123",
})
const loginRes = http.post(`${BASE_URL}/api/auth/login`, loginPayload, {
headers: { "Content-Type": "application/json" },
})
if (loginRes.status === 200) {
return { token: loginRes.json("access_token") }
}
throw new Error("Authentication failed in setup")
}
export default function (data) {
const headers = {
Authorization: `Bearer ${data.token}`,
"Content-Type": "application/json",
}
group("API Health", () => {
const healthRes = http.get(`${BASE_URL}/health`)
check(healthRes, {
"health check is 200": (r) => r.status === 200,
})
})
group("Posts API", () => {
// List posts
const listRes = http.get(`${BASE_URL}/api/posts`, { headers })
const listSuccess = check(listRes, {
"list posts is 200": (r) => r.status === 200,
"has posts array": (r) => Array.isArray(r.json("data")),
})
if (!listSuccess) apiErrors.add(1)
apiDuration.add(listRes.timings.duration)
sleep(1)
// Create post
const createPayload = JSON.stringify({
content: `Test post ${Date.now()}`,
platforms: ["twitter", "linkedin"],
scheduled_at: new Date(Date.now() + 3600000).toISOString(),
})
const createRes = http.post(`${BASE_URL}/api/posts`, createPayload, {
headers,
})
check(createRes, {
"create post is 201": (r) => r.status === 201,
"has post id": (r) => r.json("id") !== undefined,
})
})
group("Media API", () => {
const mediaRes = http.get(`${BASE_URL}/api/media`, { headers })
check(mediaRes, {
"list media is 200": (r) => r.status === 200,
})
})
group("Dashboard API", () => {
const dashboardRes = http.get(`${BASE_URL}/api/dashboard/stats`, {
headers,
})
check(dashboardRes, {
"dashboard stats is 200": (r) => r.status === 200,
})
})
sleep(2)
}Step 5: Create Stress Test
Create k6/stress-test.js:
import http from "k6/http"
import { check, sleep } from "k6"
import { BASE_URL } from "./config.js"
export const options = {
stages: [
{ duration: "2m", target: 100 }, // Ramp up to 100 users
{ duration: "5m", target: 100 }, // Stay at 100
{ duration: "2m", target: 200 }, // Spike to 200
{ duration: "5m", target: 200 }, // Stay at 200
{ duration: "2m", target: 300 }, // Push to 300
{ duration: "5m", target: 300 }, // Stay at 300
{ duration: "5m", target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ["p(99)<3000"],
http_req_failed: ["rate<0.1"],
},
}
export function setup() {
const loginRes = http.post(
`${BASE_URL}/api/auth/login`,
JSON.stringify({
email: "admin@example.com",
password: "admin123",
}),
{ headers: { "Content-Type": "application/json" } }
)
return { token: loginRes.json("access_token") }
}
export default function (data) {
const headers = {
Authorization: `Bearer ${data.token}`,
}
// Simulate realistic user behavior
const endpoints = [
"/api/posts",
"/api/media",
"/api/dashboard/stats",
"/api/social-accounts",
"/api/analytics",
]
const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)]
const res = http.get(`${BASE_URL}${endpoint}`, { headers })
check(res, {
"status is 200": (r) => r.status === 200,
"response time OK": (r) => r.timings.duration < 3000,
})
sleep(Math.random() * 3 + 1) // Random sleep 1-4 seconds
}Step 6: Create 200 Users Test
Create k6/200-users-test.js:
import http from "k6/http"
import { check, group, sleep } from "k6"
import { SharedArray } from "k6/data"
import { BASE_URL } from "./config.js"
// Generate test users
const users = new SharedArray("users", function () {
const userList = []
for (let i = 0; i < 200; i++) {
userList.push({
email: `user${i}@test.com`,
password: "Test123!@#",
name: `Test User ${i}`,
})
}
return userList
})
export const options = {
scenarios: {
concurrent_users: {
executor: "per-vu-iterations",
vus: 200,
iterations: 1,
maxDuration: "10m",
},
},
thresholds: {
http_req_duration: ["p(95)<2000"],
http_req_failed: ["rate<0.05"],
},
}
export default function () {
const user = users[__VU - 1] // Each VU gets a unique user
group("User Registration and Activity", () => {
// Register
const signupRes = http.post(
`${BASE_URL}/api/auth/signup`,
JSON.stringify(user),
{ headers: { "Content-Type": "application/json" } }
)
const signupSuccess = check(signupRes, {
"signup successful": (r) => r.status === 201,
})
if (!signupSuccess) {
console.error(`Signup failed for ${user.email}`)
return
}
sleep(1)
// Login
const loginRes = http.post(
`${BASE_URL}/api/auth/login`,
JSON.stringify({
email: user.email,
password: user.password,
}),
{ headers: { "Content-Type": "application/json" } }
)
const loginSuccess = check(loginRes, {
"login successful": (r) => r.status === 200,
})
if (!loginSuccess) {
console.error(`Login failed for ${user.email}`)
return
}
const token = loginRes.json("access_token")
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
}
sleep(2)
// Perform various actions
http.get(`${BASE_URL}/api/posts`, { headers })
sleep(1)
http.get(`${BASE_URL}/api/dashboard/stats`, { headers })
sleep(1)
http.get(`${BASE_URL}/api/social-accounts`, { headers })
sleep(1)
// Create a post
http.post(
`${BASE_URL}/api/posts`,
JSON.stringify({
content: `Post from ${user.name}`,
platforms: ["twitter"],
}),
{ headers }
)
sleep(2)
})
}Step 7: Create Test Runner Script
Create scripts/run-k6-tests.sh:
#!/bin/bash
set -e
echo "🚀 Starting k6 Load Tests for GRIT Framework"
echo "=============================================="
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Check if k6 is installed
if ! command -v k6 &> /dev/null; then
echo -e "${RED}❌ k6 is not installed${NC}"
echo "Install it from: https://k6.io/docs/getting-started/installation/"
exit 1
fi
# Check if services are running
echo -e "${YELLOW}📋 Checking if services are running...${NC}"
if ! curl -s http://localhost:8080/health > /dev/null; then
echo -e "${RED}❌ API is not running on port 8080${NC}"
echo "Start it with: docker-compose up -d"
exit 1
fi
echo -e "${GREEN}✅ Services are running${NC}"
echo ""
# Function to run a test
run_test() {
local test_name=$1
local test_file=$2
echo -e "${YELLOW}🧪 Running ${test_name}...${NC}"
k6 run --out json=k6/results/${test_name}-$(date +%Y%m%d-%H%M%S).json ${test_file}
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ ${test_name} completed successfully${NC}"
else
echo -e "${RED}❌ ${test_name} failed${NC}"
fi
echo ""
}
# Create results directory
mkdir -p k6/results
# Run tests based on argument
case "${1:-all}" in
auth)
run_test "Authentication Test" "k6/auth-test.js"
;;
api)
run_test "API Load Test" "k6/api-test.js"
;;
stress)
run_test "Stress Test" "k6/stress-test.js"
;;
200users)
run_test "200 Users Test" "k6/200-users-test.js"
;;
all)
run_test "Authentication Test" "k6/auth-test.js"
sleep 5
run_test "API Load Test" "k6/api-test.js"
sleep 5
run_test "Stress Test" "k6/stress-test.js"
;;
*)
echo "Usage: $0 {auth|api|stress|200users|all}"
exit 1
;;
esac
echo -e "${GREEN}🎉 All tests completed!${NC}"
echo "Results saved in k6/results/"Make it executable:
chmod +x scripts/run-k6-tests.shStep 8: Running the Tests
Start Your Services
# Start all services
docker-compose up -d
# Verify services are running
docker-compose psRun Individual Tests
# Authentication test
./scripts/run-k6-tests.sh auth
# API load test
./scripts/run-k6-tests.sh api
# Stress test
./scripts/run-k6-tests.sh stress
# 200 concurrent users test
./scripts/run-k6-tests.sh 200usersRun All Tests
./scripts/run-k6-tests.sh allRun with Custom Options
# Override base URL
k6 run -e BASE_URL=https://api.production.com k6/api-test.js
# Run with more VUs
k6 run --vus 500 --duration 5m k6/stress-test.js
# Output to InfluxDB
k6 run --out influxdb=http://localhost:8086/k6 k6/api-test.jsStep 9: Integrate with Grafana Cloud
1. Sign Up for Grafana Cloud
Visit grafana.com/products/cloud and create a free account.
2. Get Your k6 Cloud Token
# Login to k6 cloud
k6 login cloud
# Or set token directly
export K6_CLOUD_TOKEN=your_token_here3. Run Tests with Cloud Output
k6 run --out cloud k6/api-test.js4. View Results in Grafana
Your test results will be available at: https://app.k6.io/runs/YOUR_RUN_ID
Step 10: CI/CD Integration
GitHub Actions Example
Create .github/workflows/k6-tests.yml:
name: k6 Load Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: "0 2 * * *" # Run daily at 2 AM
jobs:
k6-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: grit
POSTGRES_PASSWORD: grit
POSTGRES_DB: social-media-manager
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Start API Server
run: |
cd apps/api
go run cmd/server/main.go &
sleep 10
env:
DATABASE_URL: postgres://grit:grit@localhost:5432/social-media-manager?sslmode=disable
REDIS_URL: redis://localhost:6379
- name: Run k6 Tests
run: |
k6 run --out json=results.json k6/api-test.js
env:
BASE_URL: http://localhost:8080
- name: Upload Results
uses: actions/upload-artifact@v3
if: always()
with:
name: k6-results
path: results.jsonUnderstanding k6 Metrics
Key Metrics
- http_req_duration: Total time for the request (sending, waiting, receiving)
- http_req_blocked: Time spent blocked before initiating request
- http_req_connecting: Time spent establishing TCP connection
- http_req_sending: Time spent sending data
- http_req_waiting: Time spent waiting for response (TTFB)
- http_req_receiving: Time spent receiving response data
- http_req_failed: Rate of failed requests
- iterations: Number of times VU executed the default function
- vus: Current number of active virtual users
Thresholds
Thresholds define pass/fail criteria:
thresholds: {
// 95% of requests must complete within 500ms
'http_req_duration': ['p(95)<500'],
// 99% of requests must complete within 2s
'http_req_duration': ['p(99)<2000'],
// Error rate must be below 1%
'http_req_failed': ['rate<0.01'],
// Average request duration must be below 300ms
'http_req_duration': ['avg<300'],
}Best Practices
1. Start Small
Begin with low VU counts and gradually increase to find your breaking point.
2. Use Realistic Scenarios
Model actual user behavior with appropriate think times (sleep).
3. Monitor System Resources
Watch CPU, memory, and database connections during tests.
4. Test in Isolation
Run tests in a dedicated environment to avoid interference.
5. Analyze Trends
Compare results over time to catch performance regressions.
6. Set Meaningful Thresholds
Base thresholds on your SLAs and user expectations.
Troubleshooting
High Error Rates
# Check API logs
docker-compose logs api
# Check database connections
docker-compose exec postgres psql -U grit -d social-media-manager -c "SELECT count(*) FROM pg_stat_activity;"Slow Response Times
# Check Redis performance
docker-compose exec redis redis-cli INFO stats
# Monitor database queries
docker-compose logs postgres | grep "duration:"Connection Timeouts
Increase timeouts in k6:
export const options = {
httpDebug: "full",
timeout: "60s",
}Conclusion
You now have a complete k6 load testing setup for your GRIT framework. This implementation allows you to:
- Test authentication flows
- Validate API performance under load
- Identify bottlenecks before production
- Integrate performance testing into CI/CD
- Monitor trends with Grafana Cloud
Regular load testing ensures your application can handle real-world traffic and provides confidence when scaling.
Resources
Found this helpful? Check out more articles on my portfolio at yourportfolio.com