|
1 | | -# Load Testing - Student Management API |
| 1 | +# Flask REST API Load Testing |
| 2 | + |
| 3 | +This repository contains load testing scripts for the Student Management Flask API, which manages student records using PostgreSQL. |
2 | 4 |
|
3 | 5 | ## Overview |
4 | 6 |
|
5 | | -This directory contains load testing scripts for the Student Management API. We use Locust to simulate concurrent users and measure API performance under load. |
| 7 | +The API provides endpoints to manage students: |
6 | 8 |
|
7 | | -The load test covers the following endpoints: |
| 9 | +Method Endpoint Description |
| 10 | +GET / Home page |
| 11 | +GET /health Health check |
| 12 | +GET /students Get all students |
| 13 | +POST /students Add a new student |
| 14 | +GET /students/<id> Get a single student |
| 15 | +PUT /students/<id> Update a student |
| 16 | +DELETE /students/<id> Delete a student |
8 | 17 |
|
9 | | -- `GET /students` — Retrieve all students |
10 | | -- `POST /students` — Create a new student |
| 18 | +## Load Testing Goals: |
11 | 19 |
|
12 | | -## Prerequisites |
| 20 | +- Test the performance and stability of the API under concurrent requests. |
| 21 | +- Measure request throughput (requests per second), response times, and failure rates. |
| 22 | +- Identify potential bottlenecks for GET and POST requests. |
| 23 | +- Verify that the API can handle both read and write-heavy traffic. |
13 | 24 |
|
14 | | -- Python 3.10+ |
15 | | -- Locust 2.x |
16 | | -- The Flask API must be running locally or remotely |
| 25 | +## Setup |
17 | 26 |
|
18 | | -Install Locust if not already installed: |
| 27 | +Activate virtual environment: |
19 | 28 |
|
20 | 29 | ```bash |
21 | | -pip install locust |
| 30 | +source .venv/bin/activate |
22 | 31 | ``` |
23 | 32 |
|
24 | | -## Running the Load Test |
25 | | - |
26 | | -1. Start your Flask API: |
| 33 | +Install dependencies: |
27 | 34 |
|
28 | 35 | ```bash |
29 | | -export FLASK_APP=app:create_app |
30 | | -export FLASK_ENV=development |
31 | | -flask run --host=0.0.0.0 --port=5000 |
| 36 | +pip install -r requirements.txt |
| 37 | +pip install locust |
32 | 38 | ``` |
33 | 39 |
|
34 | | -2. Open a new terminal and navigate to the `tests/` directory: |
| 40 | +Ensure API is running: |
35 | 41 |
|
36 | 42 | ```bash |
37 | | -cd tests |
| 43 | +flask run --host=0.0.0.0 --port=5000 |
38 | 44 | ``` |
39 | 45 |
|
40 | | -3. Run Locust: |
| 46 | +Verify /students and /health endpoints are accessible. |
| 47 | + |
| 48 | +## Load Testing with Locust |
| 49 | + |
| 50 | +### Locust Test Script |
| 51 | + |
| 52 | +The load test script is located at `tests/load_test.py`. |
| 53 | + |
| 54 | +It simulates Student API users performing: |
| 55 | + |
| 56 | +- GET /students |
| 57 | +- POST /students with random student data |
| 58 | + |
| 59 | +(Optional: can add PUT, DELETE, GET /students/<id>) |
| 60 | + |
| 61 | +Run Locust: |
41 | 62 |
|
42 | 63 | ```bash |
| 64 | +cd tests |
43 | 65 | locust -f load_test.py --host=http://localhost:5000 --web-host 0.0.0.0 |
44 | 66 | ``` |
45 | 67 |
|
46 | | -4. Access the web interface in your browser and specify number of users and spawn rate: |
| 68 | +Locust web UI will be available at `http://<server-ip>:8089`. |
47 | 69 |
|
48 | | -``` |
49 | | -http://<your-server-ip>:8089 |
50 | | -``` |
| 70 | +Configure number of users and spawn rate from the UI. |
51 | 71 |
|
52 | | -Start the test from the web UI. |
| 72 | +### Endpoints Tested in Locust |
53 | 73 |
|
54 | | -## Load Test Script (tests/load_test.py) |
| 74 | +- `/` (Home) — lightweight endpoint |
| 75 | +- `/health` (Health check) — lightweight endpoint |
| 76 | +- `/students` (GET/POST) — core student API |
| 77 | +- `/students/<id>` (GET/PUT/DELETE) — single student operations |
55 | 78 |
|
56 | | -```python |
57 | | -from locust import HttpUser, task, between |
58 | | -import random |
| 79 | +## Observations from Load Testing |
59 | 80 |
|
60 | | -class StudentApiUser(HttpUser): |
61 | | - wait_time = between(1, 2) |
| 81 | +### GET / and /health |
62 | 82 |
|
63 | | - @task(2) |
64 | | - def get_students(self): |
65 | | - self.client.get("/students") |
| 83 | +- All requests succeeded. |
| 84 | +- Median response: 150–200ms. |
| 85 | +- Low payload, very fast and reliable. |
66 | 86 |
|
67 | | - @task(1) |
68 | | - def create_student(self): |
69 | | - student_id = random.randint(1000, 9999) |
70 | | - payload = { |
71 | | - "name": f"Test User {student_id}", |
72 | | - "domain": "Engineering", |
73 | | - "gpa": round(random.uniform(6.0, 10.0), 2), |
74 | | - "email": f"testuser{student_id}@example.com" |
75 | | - } |
76 | | - self.client.post("/students", json=payload) |
77 | | -``` |
| 87 | +### GET /students |
78 | 88 |
|
79 | | -## Observations from Load Testing |
| 89 | +- Handles ~13 requests/sec for 1,300+ records. |
| 90 | +- Median response ≈ 291ms. |
| 91 | +- 95th percentile ≈ 780ms (due to large payloads). |
| 92 | + |
| 93 | +### POST /students |
80 | 94 |
|
81 | | -The load test was run with 50 concurrent users and a spawn rate of 5 users per second. |
| 95 | +- Some failures observed due to duplicate emails (email is unique in DB). |
| 96 | +- Handles ~5–6 successful requests/sec with random payload. |
| 97 | +- Median response ≈ 325ms; maximum ≈ 1.6s. |
| 98 | +- Recommendation: generate unique test data for high-volume writes. |
82 | 99 |
|
83 | | -### Summary Table |
| 100 | +### Single student operations |
84 | 101 |
|
85 | | -| Request | # Requests | # Failures | Median (ms) | Average (ms) | Min (ms) | Max (ms) | Avg Size (bytes) | |
86 | | -|---------------|------------|------------|-------------|--------------|----------|----------|-------------------| |
87 | | -| GET /students | 1257 | 0 | 13 | 17.87 | 5 | 159 | 40932.49 | |
88 | | -| POST /students| 601 | 19 | 8 | 11.65 | 6 | 78 | 1934.76 | |
89 | | -| **Aggregated**| 1858 | 19 | 11 | 15.86 | 5 | 159 | 28318.05 | |
| 102 | +- GET/PUT/DELETE /students/<id> are extremely fast (avg ~20ms). |
| 103 | +- Some GET failures occur if the student ID was deleted during the test. |
90 | 104 |
|
91 | | -### Observations |
| 105 | +### Overall |
92 | 106 |
|
93 | | -- GET Requests: |
94 | | - - No failures occurred. |
95 | | - - Response times were fast (median: 13 ms, average: 17.87 ms), showing the API handles read-heavy operations well. |
| 107 | +- Aggregated throughput: ~32–35 requests/sec across endpoints. |
| 108 | +- Median response for all endpoints ≈ 48ms. |
| 109 | +- POST operations require better handling for unique constraints. |
| 110 | +- API is stable and performant for read-heavy workloads. |
96 | 111 |
|
97 | | -- POST Requests: |
98 | | - - A small number of failures (19) occurred out of 601 requests (~3% failure rate). |
99 | | - - Response times were slightly lower than GET requests (median: 8 ms, average: 11.65 ms), but failures indicate occasional issues with creating new entries under load. |
| 112 | +## Load Testing Best Practices |
100 | 113 |
|
101 | | -- Overall Performance: |
102 | | - - The API handled approximately 32 requests/sec (aggregated) under this simulated load. |
103 | | - - Response times stayed under 200 ms for both GET and POST requests, indicating good responsiveness. |
104 | | - - Failures on POST requests may require investigation into database constraints, concurrency handling, or server resource limits. |
| 114 | +- Use unique test data for POST requests to avoid conflicts. |
| 115 | +- Seed the database with realistic test data (`seed.py`) before running tests. |
| 116 | +- Monitor database performance under concurrent writes. |
| 117 | +- Incrementally increase users in Locust to observe scaling limits. |
| 118 | +- Measure 95th percentile response times for production-level performance. |
105 | 119 |
|
106 | | -## Recommendations |
| 120 | +## Generating Test Data |
107 | 121 |
|
108 | | -- Investigate POST failures to identify whether they are caused by: |
109 | | - - database constraints (unique indexes, FK constraints), |
110 | | - - race conditions during concurrent inserts, |
111 | | - - connection/timeouts to the database, |
112 | | - - or insufficient server resources under burst load. |
| 122 | +Seed the database with dummy students: |
113 | 123 |
|
114 | | -- Consider: |
115 | | - - implementing database connection pooling, |
116 | | - - adding retries for transient failures (with exponential backoff), |
117 | | - - validating and sanitizing payloads before inserting, |
118 | | - - introducing optimistic locking or deduplication where appropriate. |
| 124 | +```bash |
| 125 | +python seed.py |
| 126 | +``` |
| 127 | + |
| 128 | +This will insert 100 dummy students with unique emails. |
| 129 | + |
| 130 | +## Running Advanced Tests |
119 | 131 |
|
120 | | -- Run additional tests: |
121 | | - - with higher concurrency to observe scaling limits, |
122 | | - - with realistic user behavior (think time, different endpoints), |
123 | | - - varying payload sizes and database load. |
| 132 | +To load test all endpoints including `/students/<id>` operations: |
124 | 133 |
|
125 | | -- Use the Locust web interface to continuously monitor: |
126 | | - - requests/sec (RPS), |
127 | | - - failure rates, |
128 | | - - response-time percentiles (median, 95th, 99th), |
129 | | - - and request/response sizes. |
| 134 | +Modify `load_test.py` to: |
130 | 135 |
|
131 | | -## Notes |
| 136 | +- Maintain a list of existing student IDs. |
| 137 | +- Perform GET/PUT/DELETE operations on valid IDs. |
| 138 | +- Avoid None or deleted IDs to reduce failures. |
132 | 139 |
|
133 | | -- Keep test environment as close to production as possible for meaningful results (same DB size/config, same instance types). |
134 | | -- Capture and inspect server logs during the test to correlate errors and stack traces with failing requests. |
135 | | -- If failures persist, run a focused test targeting POSTs to reproduce and capture detailed error responses from the API. |
| 140 | +Run Locust as usual: |
| 141 | + |
| 142 | +```bash |
| 143 | +locust -f tests/load_test.py --host=http://localhost:5000 --web-host 0.0.0.0 |
| 144 | +``` |
0 commit comments