Objective: Make a deliberately under-optimised application survive 5,000 concurrent users against a single, constrained MongoDB node.
- Overview & Rules
- System Architecture
- Prerequisites β Installing the Tools
- Environment Setup
- Verifying the Environment
- Running the Stress Test (Baseline)
- Your Task β Optimisation Targets
- Pass Criteria
- Submission Checklist
- Hints (Read Only When Stuck)
You are given a small web service connected to a MongoDB database. Both are deployed inside a local Kubernetes cluster (k3d). The system, as provided, will collapse under load. Your job is to fix it.
| Constraint | Value |
|---|---|
| MongoDB nodes | 1 (no horizontal scaling) |
| MongoDB memory limit | 500 MiB |
| MongoDB IOPS cap (simulated) | 100 concurrent I/O tickets |
Reads per /api/data request |
5 (fixed in source code) |
Writes per /api/data request |
5 (fixed in source code) |
- Application code (within the language you choose: Python or Node.js)
- Dockerfile and container configuration
- Number of application pod replicas
- Kubernetes resource requests/limits for application pods only
- Kubernetes manifests for the application (not the MongoDB CPU and memory limit)
- Introduce a caching layer (Redis, Memcached, etc.) β but you must deploy it inside the cluster
- Ingress/proxy configuration
- Anything else not listed in the "Hard Rules" above
k8s/mongodb/deployment.yamlβ MongoDB replicas, memory limits, IOPS constraints- The count of reads/writes inside the
/api/datahandler (theforloop bounds) - The Kubernetes Namespace name (
assessment)
ββββββββββββββββββββββββββββββββββββββββββββββββ
β k3d Cluster (local) β
β β
Browser / k6 β ββββββββββββββββ βββββββββββββββββββ β
βββββββββββββββΊ 80 β β Traefik ββββββΊβ App (Python β β
(assessment.local) β β Ingress β β or Node.js) β β
β ββββββββββββββββ β replicas: 1 β β
β ββββββββββ¬βββββββββ β
β β β
β ββββββββββΌβββββββββ β
β β MongoDB β β
β β 1 node β β
β β 500 MiB RAM β β
β β ~100 IOPS β β
β βββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββ
| Endpoint | Description |
|---|---|
GET /healthz |
Liveness probe β always returns 200 |
GET /readyz |
Readiness probe β checks MongoDB connectivity |
GET /api/data |
Assessment endpoint β 5 reads + 5 writes per call |
GET /api/stats |
Collection document count |
Docker must be installed and the daemon must be running before you proceed.
- Linux: https://docs.docker.com/engine/install/
- macOS / Windows: Install Docker Desktop
Verify:
docker versionk3d runs a full k3s Kubernetes cluster inside Docker containers β no VMs needed.
# Via the official install script
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
# Or via Homebrew on Linux
brew install k3dVerify:
k3d version# Homebrew (recommended)
brew install k3d
# Or via the install script
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bashVerify:
k3d versionOption A β winget (Windows Package Manager)
winget install k3dOption B β Chocolatey
choco install k3dOption C β Manual
- Go to https://github.com/k3d-io/k3d/releases/latest
- Download
k3d-windows-amd64.exe - Rename it to
k3d.exeand place it in a directory on yourPATH(e.g.C:\tools\)
Verify (PowerShell):
k3d version# Via pacman (community repo)
sudo pacman -S k3d
# Or AUR
yay -S k3d-binVerify:
k3d versionkubectl is the Kubernetes command-line tool.
# Download the latest stable release
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
# Arch Linux
sudo pacman -S kubectl
# Via Homebrew on Linux
brew install kubectlbrew install kubectl# winget
winget install Kubernetes.kubectl
# Chocolatey
choco install kubernetes-cli
# Or download manually from:
# https://dl.k8s.io/release/v1.30.0/bin/windows/amd64/kubectl.exeVerify (all platforms):
kubectl version --clientk6 is used to run the included stress test.
# 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 k6
# Fedora/RHEL/CentOS
sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6
# Arch Linux
yay -S k6-bin
# Via Homebrew on Linux
brew install k6brew install k6# winget
winget install k6
# Chocolatey
choco install k6Verify (all platforms):
k6 versiongit clone <repo-url>
cd devops-assessmentThe Ingress uses the hostname assessment.local. Add it to your hosts file:
Linux / macOS:
echo "127.0.0.1 assessment.local" | sudo tee -a /etc/hostsWindows (PowerShell as Administrator):
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 assessment.local"Run the provided setup script. It will:
- Create a k3d cluster with 2 agent nodes
- Build both Docker images
- Import them into the cluster (no external registry needed)
- Apply all Kubernetes manifests
- Wait for everything to be healthy
chmod +x setup.sh
./setup.shWindows users: Run the commands inside
setup.shmanually in sequence, or use Git Bash / WSL2.
Expected output (tail):
[OK] All deployments are ready!
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Assessment Environment Ready!
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Endpoints:
Health : http://assessment.local/healthz
Readiness: http://assessment.local/readyz
API : http://assessment.local/api/data
Stats : http://assessment.local/api/stats
# All pods should be Running
kubectl get pods -n assessment
# Quick smoke test
curl http://assessment.local/healthz
# β {"status":"ok","timestamp":"..."}
curl http://assessment.local/readyz
# β {"status":"ready","timestamp":"..."}
curl http://assessment.local/api/data
# β {"status":"success","reads":[...],"writes":[...],"timestamp":"..."}Arch Linux only β if curl still fails after adding the hosts entry, fix the resolver order:
sudo sed -i 's/hosts: mymachines mdns_minimal \[NOTFOUND=return\] resolve files myhostname dns/hosts: mymachines files mdns_minimal resolve myhostname dns/' /etc/nsswitch.confThis moves files before mdns_minimal so /etc/hosts is checked first.
Expected pod list:
NAME READY STATUS RESTARTS AGE
mongo-xxxxxxxxx-xxxxx 1/1 Running 0 2m
app-python-xxxxxxxxx-xxxxx 1/1 Running 0 90s
app-nodejs-xxxxxxxxx-xxxxx 1/1 Running 0 90s
Before making any changes, run the stress test to establish a baseline. It is expected to fail at this point. You should see the system breaking at 5,000 virtual users
k6 run stress-test/stress-test.jsBut it works fine at 100 concurrent users
k6 run --vus 100 --duration 30s stress-test/stress-test.jsTo run against a different URL:
BASE_URL=http://assessment.local k6 run stress-test/stress-test.jsTo enable verbose failure logging:
VERBOSE=true k6 run stress-test/stress-test.jsk6 outputs a summary table after each run. Key metrics to watch:
| Metric | What it means | Target |
|---|---|---|
http_req_duration p(95) |
95th-percentile latency | < 2,000 ms |
http_req_duration p(99) |
99th-percentile latency | < 5,000 ms |
http_req_failed |
Fraction of failed requests | < 1% |
error_rate |
Custom error counter | < 1% |
http_reqs |
Total requests completed | Higher is better |
A β next to a threshold means it passed. A β means it failed.
With 100 IOPS capped on MongoDB, you cannot sustain 5,000 raw read-write cycles per second. The system collapses under load. Find out why, fix it, and make it pass the stress test at 5,000 VUs. Identify the base system limitations, the optimized system limitations. You must deploy any new infrastructure inside the cluster as a Kubernetes Deployment + Service.
You have four layers to investigate:
Application β the app code and how it talks to MongoDB
Infrastructure β how the app is deployed inside the cluster
Database β how MongoDB is being used (within the hard constraints)
Container β how the images are built and run
Where you start and what you change is up to you. Diagnose first, then optimise.
Pick one application to optimise (Python or Node.js). Both are deployed; only the active one (default: Python) receives ingress traffic.
To switch to Node.js, edit k8s/app/services.yaml:
# Change:
name: app-python
# To:
name: app-nodejsThen apply: kubectl apply -f k8s/app/services.yaml
Your submission passes if a full k6 run stress-test/stress-test.js run at 5,000 VUs produces:
| Threshold | Required Value |
|---|---|
http_req_duration p(95) |
β€ 2,000 ms |
http_req_duration p(99) |
β€ 5,000 ms |
http_req_failed rate |
< 1% |
error_rate |
< 1% |
All four thresholds must pass simultaneously (k6 will print β or β for each). If you could not pass all the tests, clearly identify why and state that in your solution file, all submissions would be reviewed.
- All modified files committed to git
- A
SOLUTION.mdfile at the root of the repo explaining:- What changes you made and why
- What bottlenecks you identified and how you diagnosed them
- Trade-offs you considered but did not implement
- k6 summary output pasted (showing all β thresholds)
- All changes deployable via
kubectl apply(no manual steps not documented) -
setup.shstill works on a fresh cluster - You must test your solution before submission and include a screenshot of your results
One approach worth considering is decoupling the write path using a message queue. Rather than every request writing directly and synchronously to MongoDB, writes can be published to a queue and consumed asynchronously β smoothing out the burst pressure on the database.
Google Cloud Pub/Sub is one such system. For local development inside the cluster, Google provides an official Pub/Sub emulator image that behaves identically to the real service and can be deployed in Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pubsub-emulator
namespace: assessment
spec:
replicas: 1
selector:
matchLabels:
app: pubsub-emulator
template:
metadata:
labels:
app: pubsub-emulator
spec:
containers:
- name: pubsub-emulator
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
command:
- gcloud
- beta
- emulators
- pubsub
- start
- --host-port=0.0.0.0:8085
- --project=assessment-project
ports:
- containerPort: 8085
resources:
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: pubsub-emulator
namespace: assessment
spec:
selector:
app: pubsub-emulator
ports:
- port: 8085
targetPort: 8085To point your application at the emulator, set the environment variable:
PUBSUB_EMULATOR_HOST=pubsub-emulator:8085
The Google Cloud Pub/Sub client libraries for both Python and Node.js will automatically detect this variable and route all calls to the emulator instead of the real service β no code changes needed to switch between local and production.
How you integrate it, what you queue, and how you consume it is your decision to make.
# Watch pods in real time
kubectl get pods -n assessment -w
# View application logs
kubectl logs -n assessment deploy/app-python -f
kubectl logs -n assessment deploy/app-nodejs -f
kubectl logs -n assessment deploy/mongo -f
# Resource usage (requires metrics-server)
kubectl top pods -n assessment
kubectl top nodes
# Scale the app manually (without HPA)
kubectl scale deployment app-python -n assessment --replicas=5
# Exec into a running pod
kubectl exec -it -n assessment deploy/app-python -- bash
kubectl exec -it -n assessment deploy/mongo -- mongosh
# Re-import image after rebuilding
docker build -t assessment/app-python:latest ./app-python/
k3d image import assessment/app-python:latest --cluster assessment
kubectl rollout restart deployment/app-python -n assessment
# Delete and recreate the cluster (full reset)
k3d cluster delete assessment
./setup.sh
# Run a quick load sanity check (100 VUs, 30 s) before the full test
k6 run --vus 100 --duration 30s stress-test/stress-test.jsGood luck. The system is broken by design β your job is to make it unbreakable.