Scale Testing
Validating platform capacity with concurrent virtual users, automated data seeding, and real-time performance measurement.
flowchart LR
subgraph PREP[Preparation]
MOCK[Mock FMP Client<br/>synthetic financial data]:::svc
SEED[Data Seeder<br/>accounts - folders - files]:::svc
ENV[Env Setup<br/>P1V2 - rate limit raised]:::ext
end
subgraph RUN[Load Run Phases]
P1[Baseline<br/>10 users - 2 min]:::app
P2[Moderate<br/>20 users - 5 min]:::app
P3[Heavy<br/>50 users - 10 min]:::app
P4[Claude AI<br/>10 users - 5 min]:::sec
end
subgraph METRICS[Metrics Collected]
RT[Real-Time Stats<br/>every 10 sec]:::data
SUM[Final Summary<br/>P50 - P95 - P99]:::data
CSV[CSV Export]:::data
end
MOCK --> SEED
SEED --> ENV
ENV --> P1
P1 --> P2
P2 --> P3
P3 --> P4
P4 --> RT
RT --> SUM
SUM --> CSV
classDef app fill:#1f2630,stroke:#C0894E,color:#F3EFE8;
classDef svc fill:#241c14,stroke:#E2B583,color:#F3EFE8;
classDef data fill:#22201a,stroke:#C0894E,color:#EBD3AE;
classDef ext fill:#1b1f27,stroke:#5A616B,color:#C4C0B8;
classDef sec fill:#1d2733,stroke:#5cc8e0,color:#eafaff;
Why Scale Test?
Before launching a SaaS platform, you need to know it can handle real-world load. Scale testing answers critical questions: How many concurrent users can the infrastructure support? Where are the bottlenecks? Will the database hold up under pressure? Will API rate limits protect the system or choke legitimate traffic?
Grade My Investments uses a purpose-built scale testing framework consisting of three tools that work together to seed synthetic data, simulate concurrent user sessions, and measure performance metrics with percentile-level precision.
Scale Testing Toolchain
The scale testing framework is composed of three dedicated .NET console applications, each handling a specific phase of the testing lifecycle:
Mock FMP Client
Generates realistic synthetic financial data so report generation runs without consuming real API credits. Deterministic per symbol for repeatable tests.
MOCK_FMP=trueData Seeder
Creates synthetic test accounts, folders, and files via the API. Supports configurable account counts, folder depth, and file density.
ScaleTest.Console seedLoad Runner
Simulates N concurrent virtual users performing realistic workflows against the API. Captures latency percentiles and error rates in real time.
ScaleTestRunner.Console runVirtual User Simulation
Each virtual user runs as an independent concurrent session, executing a realistic workflow loop that mimics how real users interact with the platform. The simulation includes configurable "think time" delays between actions to model human browsing behavior.
Workflow Per Virtual User
Pick from the pool of seeded test accounts to distribute load across data partitions.
GET /accounts/{"{id}"}/folder-tree — Fetches the full hierarchical folder structure with nested files.
GET /folders/{"{id}"}/files — Retrieves file metadata for a randomly selected folder.
GET /files/{"{id}"}/download — Downloads binary file content from Azure Blob Storage.
Configurable random delay (default 500ms–2000ms) simulating human interaction pauses.
POST /accounts/{"{id}"}/downloads — Requests a ZIP package of an entire folder.
Loop continuously until the test duration expires. Each iteration is independent.
Test Phases
Scale tests are run in progressive phases, each increasing concurrency to identify the point where performance degrades:
| Phase | Concurrent Users | Duration | Purpose |
|---|---|---|---|
| Baseline | 10 | 2 minutes | Establish performance baselines and verify test harness |
| Moderate | 20 | 5 minutes | Normal operating load for early platform adoption |
| Heavy | 50 | 10 minutes | Stress test to find bottlenecks and breaking points |
| Claude AI | 10 | 5 minutes | Test Claude AI analysis endpoints under concurrent load |
Performance Metrics Collected
The load runner captures detailed performance metrics for every API call, providing both real-time visibility and comprehensive post-test analysis:
Real-Time Stats (Every 10 Seconds)
- Total request count
- Requests per second (RPS)
- Average latency
- Error count and error rate percentage
Final Summary Report
- Per-endpoint breakdown (count, avg, P50, P95, P99, min, max)
- HTTP status code distribution
- Error categorization (rate limits, server errors, timeouts)
- CSV export for further analysis
Latency Percentiles
Averages can be misleading — a few slow requests can hide behind a fast average. That's why Grade My Investments tracks P50 (median), P95 (95th percentile), and P99 (99th percentile) latencies per endpoint. P99 tells you what your slowest 1% of users experience.
| Metric | What It Measures | Target |
|---|---|---|
| P50 | Median response time — half of requests are faster than this | < 300ms |
| P95 | 95th percentile — only 5% of requests are slower | < 1,000ms |
| P99 | 99th percentile — captures worst-case user experience | < 2,000ms |
| Error Rate | Percentage of non-200/201 responses | < 1% |
Automated Data Seeding
Realistic scale testing requires realistic data. The Data Seeder creates synthetic accounts, folder hierarchies, and uploaded files via the same API endpoints that real users would use. This exercises the full request pipeline including authentication, validation, database writes, and blob storage uploads.
Seeder Capabilities
| Parameter | Default | Description |
|---|---|---|
--accounts |
10 | Number of synthetic test accounts to create |
--folders-per-account |
5 | Folder hierarchy depth per account |
--files-per-folder |
3 | Files uploaded per folder (uses real template files) |
--prefix |
scaletest | Naming prefix for easy identification and cleanup |
cleanup command removes all seeded data by prefix.
Billing Test Data
A dedicated billing-seed command extends the seeded accounts with billing data for end-to-end payment testing:
- Enables and validates each test account
- Adds Stripe test credit cards (Visa ****4242)
- Creates randomized report generation charges ($3–$75 per charge)
- Creates storage charges for billing cycle testing
Mock Financial Data Provider
The report engine's largest external dependency is the Financial Modeling Prep (FMP) API, which provides market data and financial statements. For scale testing, a mock implementation generates synthetic but realistic financial data without making real API calls.
Mock FMP Features
Same symbol always returns same data. Uses symbol-based RNG seeding for repeatable tests.
Prices $15–$500, realistic margins (30–60% gross), proper balance sheet ratios.
Adds 10–50ms random delays to simulate real API response times.
All 13 FMP endpoints mocked: prices, financials, balance sheets, cash flows, metrics, analyst data.
Infrastructure Preparation
Before running scale tests, the target environment is prepared to isolate performance measurements from configuration artifacts:
| Step | Action | Reason |
|---|---|---|
| 1 | Upgrade App Service to P1V2 | Remove compute ceiling so we measure database/storage limits, not CPU |
| 2 | Raise API rate limit | Prevent rate limiter from masking real performance bottlenecks |
| 3 | Disable admin notification emails | Prevent email flood from thousands of test operations |
| 4 | Seed test data | Create realistic data volume for meaningful results |
| 5 | Run progressive test phases | Gradually increase load to find the breaking point |
| 6 | Restore original settings | Return environment to normal operating configuration |
Concurrent Users to Platform Users
Scale test "concurrent users" are far more aggressive than real users. The test runner uses 500ms–2,000ms think times, while real users typically pause 30–60 seconds between actions. This means each concurrent test user represents approximately 15–30 real platform users.
| Concurrent Test Users | Real Platform Users | Assessment |
|---|---|---|
| 10 | 100–200 | Comfortable |
| 20–25 | 200–500 | Maximum clean capacity |
| 30 | 300–600 | Starting to see pressure |
| 50 | 500–1,000 | Requires connection pool tuning |
Quick Facts
- 3 dedicated testing tools
- 50+ concurrent virtual users tested
- 37,000+ API requests per test run
- Percentile-level latency tracking
- Automated data seeding and cleanup
- Real-time stats every 10 seconds
- CSV export for analysis
Technology Stack
- .NET 10 Console applications
- HttpClient Concurrent sessions
- ConcurrentBag Thread-safe metrics
- System.CommandLine CLI framework
- CancellationToken Graceful shutdown
CLI Examples
# Seed 50 test accounts
dotnet run -- seed \
--accounts 50 \
--folders-per-account 5 \
--files-per-folder 5
# Run 50 concurrent users for 10 min
dotnet run -- run \
--concurrency 50 \
--duration 600 \
--think-time-min 500 \
--think-time-max 2000
# Set up billing test data
dotnet run -- billing-seed \
--charges-per-account 3
# Clean up test data
dotnet run -- cleanupBottlenecks Found
Scale testing identified these bottlenecks before they could impact production:
- MySQL connection pool — exhaustion at 50 concurrent users
- API rate limiting — default limits too restrictive for bulk operations
- Storage charge rounding — sub-penny charges violating DB constraint
- Invoice tracking — EF Core entity tracking conflict on paid status
All issues were identified and fixed during scale testing, before reaching production.
Projects
Gmi.ScaleTest.ConsoleGmi.ScaleTestRunner.ConsoleGmi.RestClient.FinancialDataProviderGmi.BillingEngine.Console