Scalability Review
You review whether the application can handle its target load and grow with demand. Scalability is not a single property — it's the absence of several specific anti-patterns.
This skill is the most scope-tier-sensitive. A prototype should not be forced to defend horizontal scaling. A scalable-tier system must.
Inputs
From orchestrator: scope_tier, stack_summary, gitnexus_indexed, entry points, and any context on expected load (QPS, data volume, tenant count, geographic distribution).
If expected load isn't known, ask — but keep it simple:
Rough scale targets for scaling review:
- Concurrent users (peak): ?
- Requests/sec at peak: ?
- Total data volume (e.g. rows in largest table): ?
- Tenants / accounts: ?
- Geographic spread: single region / multi-region / global?
Mode detection
- Plan mode — produce scalability report with prioritized findings.
- Edit mode — apply targeted fixes. Indexes, pagination, connection pool tuning, cache additions can usually be applied. Sharding, read replicas, architectural restructuring must be proposed and discussed, not applied.
Thresholds by tier
| Tier |
DB indexing |
N+1 |
Caching |
Stateless |
Rate limiting |
Pagination |
Async jobs |
Headroom |
| prototype |
advisory |
advisory |
optional |
advisory |
optional |
required on list endpoints |
optional |
advisory |
| team |
required on common queries |
required to fix |
recommended where beneficial |
required (no in-process session state) |
required on public + auth endpoints |
required |
required for long-running work |
2× expected load |
| scalable |
required + reviewed regularly |
required |
required — multi-tier, invalidation strategy |
required + tested |
required + per-tenant |
required + cursor-based for large sets |
required + queue-backed |
3-5× expected load, headroom monitored |
Review surface
1. Database schema and indexing
- Check for indexes on common access patterns. Use GitNexus
mcp__gitnexus__query if available to find all WHERE/ORDER BY/JOIN columns. Otherwise scan ORM queries / repository methods / SQL files.
- For each frequently-queried table:
- Primary key exists and is appropriate (avoid UUID PKs on InnoDB if hot insert rate is high; use ULIDs or sortable UUIDs).
- Foreign keys have matching indexes.
- Composite indexes match query predicates (column order matters — most selective first).
- Covering indexes for hot read paths where it meaningfully reduces I/O.
- No duplicate / redundant indexes.
- Anti-patterns:
SELECT * in hot paths — fetches columns you don't need; blocks covering-index optimization.
LIKE '%foo%' on un-indexed text — full scan; use full-text search or GIN/GIST (Postgres).
OR across indexed + non-indexed columns causing index skip.
ORDER BY RAND() — full scan + filesort.
- Queries without
LIMIT returning large result sets.
- Migrations:
- Adding an index on a large table without
CONCURRENTLY (Postgres) blocks writes.
- Adding NOT NULL columns with a default on a large table can rewrite the whole table (Postgres < 11).
- Long-running migrations require online-migration patterns.
2. N+1 queries
- Grep ORM call patterns for loops that trigger per-iteration queries:
- Rails:
.each { .foo.bar } without .includes(:foo) / .eager_load.
- Django: loop over
.all() accessing related fields without select_related / prefetch_related.
- SQLAlchemy: accessing relationships without
joinedload / selectinload.
- Prisma:
.findMany then .findUnique in a loop.
- ActiveRecord / similar: nested
.map with DB access inside.
- Graphql resolvers without a DataLoader for the N+1 classic case.
- Check query logs (if captured in dev): same query repeated with different parameters → N+1 signature.
3. Query analysis
- Enable query logging in dev / staging to observe real query counts per request.
- For critical endpoints: run
EXPLAIN ANALYZE (Postgres) / EXPLAIN FORMAT=JSON (MySQL). Findings if:
- Sequential scans on large tables.
- Sort operations without index support on
ORDER BY.
- Temporary tables / filesort.
- Hash joins where nested-loop would be faster (or vice-versa).
- Slow query log enabled in production at team+ tier.
4. Connection pooling
- DB connection pool present and sized appropriately:
- Pool size roughly:
(max_concurrent_requests × avg_queries_per_request × avg_query_time) / target_latency. Too-small pools cause waiting; too-large pools exhaust DB.
- At scalable tier, use a dedicated pooler (PgBouncer, Odyssey) between app and DB — especially for serverless / many-instance deployments.
- Pool timeouts configured (wait timeout, idle timeout, max lifetime).
- HTTP client pools for outbound requests — avoid creating a new client per request.
- Redis / cache pools configured.
5. Caching
A. Tiered strategy
- Browser cache (
Cache-Control, ETag, Last-Modified) on static and semi-static responses.
- CDN / edge cache for cacheable assets and API responses.
- Shared cache (Redis / Memcached) for cross-instance data.
- In-process cache for very hot read-mostly data — with clear invalidation rules.
B. What to cache
- Read-heavy queries with stable results.
- External API responses (rate-limited APIs especially).
- Computed views (denormalized aggregates).
- Per-request memoization of repeated lookups.
C. Invalidation
- Cache invalidation strategy documented — or cache drift / stale data becomes a bug source.
- Write-through / write-behind / TTL-only — which does the app use per cache? Is it appropriate to the data's staleness tolerance?
- Cache stampede prevention:
singleflight, lock-on-miss, probabilistic early expiration.
D. Anti-patterns
- Caching user-specific data in a shared cache without tenant / user segmentation (cross-tenant data leak).
TTL = Infinity without invalidation trigger.
- Caching error responses.
6. Statelessness / horizontal scaling
- In-process session state is a finding at team+ tier. Sessions in a shared store (Redis, DB, signed JWTs).
- In-process caches with local-only invalidation don't scale — adding a second instance causes divergence.
- Sticky sessions as a workaround for stateful design — flag.
- WebSocket / SSE / long-poll connections on an auto-scaled service — scaling requires a backplane (Redis pub/sub, message queue, managed service like Pusher / Ably / API Gateway WebSockets).
- Singleton state (in-process rate limiter, in-process scheduler, in-process job queue) that must be coordinated across instances.
- Container startup time — slow cold-start makes scaling reactive rather than elastic.
7. Background work and async
- Long-running work on the request path is a finding:
- Report generation, bulk operations, third-party API chains — move to background.
- Job queue in use: BullMQ, Sidekiq, Celery, RQ, SQS, GCP Tasks, Kafka, Temporal — any?
- Queue properties:
- Persistent (durable).
- At-least-once delivery + consumer idempotency.
- Dead-letter queue for unprocessable messages.
- Monitored for backlog growth.
- Autoscaling based on queue depth.
- Cron / scheduled jobs:
- Use a distributed scheduler (not per-instance cron) — avoid duplicate executions.
- Long-running cron jobs can overlap — detect and prevent.
8. Rate limiting
Overlaps with security-audit + reliability-audit. Scalability angle:
- Rate limits prevent a noisy tenant / bot / runaway-loop from degrading service for everyone.
- Per-API-key / per-IP / per-user limits at team+ tier.
- Per-tenant / per-endpoint differentiated limits at scalable tier.
- Enforced at the edge (reverse proxy, API gateway) preferred over application-layer for DDoS absorption.
- Quota + bucket semantics documented (burst, refill rate).
- 429 responses include
Retry-After header.
9. Pagination
- Every list endpoint must paginate at team+ tier.
- Cursor-based pagination for large or frequently-updated datasets (avoids offset drift + offset performance).
- Offset pagination acceptable for small bounded lists only.
- Max page size enforced on the server — client can't request 100k records.
- Total count queries are expensive on large tables — avoid or cache.
10. Search and large scans
- Avoid full-table scans in user-facing paths.
- Full-text search delegated to a dedicated system (Elasticsearch / OpenSearch / Meilisearch / Typesense / Postgres FTS with proper indexes) rather than
ILIKE '%...%' at scale.
- Aggregate / reporting queries isolated from OLTP DB (read replica, data warehouse, OLAP store) at scalable tier.
11. Concurrency + throughput
- Request-handling concurrency model appropriate to stack:
- Node.js: I/O-bound workloads benefit from high async concurrency, single thread per event loop — worker threads / cluster for CPU.
- Python: GIL-bound — use multiple processes (gunicorn workers, uvicorn workers) for CPU-bound; asyncio for I/O-bound.
- Go: default is efficient; watch for goroutine leaks.
- JVM: thread pool sizing critical — one-thread-per-request doesn't scale past pool limit.
- Blocking I/O on async runtimes is a finding — sync DB driver in an async event loop starves the loop.
12. Geographic distribution (scalable tier)
- Multi-region active-active or active-passive?
- Latency sensitivity: where do users live vs. where's the data?
- CDN + edge compute for global reads.
- Database replication — primary / replica / multi-primary? Read-your-writes consistency semantics?
- Data sovereignty constraints from compliance-check (some data can't cross borders).
13. Payload sizes
- Response sizes capped or streamed. Multi-megabyte JSON responses are a finding.
- Gzip / brotli compression enabled.
- Images served with appropriate format (WebP / AVIF) + size variants.
14. Capacity planning + autoscaling
At team+ tier:
- Autoscaling configured with sensible thresholds.
- Min instance count > 1 for availability.
- Scale-up is faster than scale-down (avoid oscillation; gentler scale-down).
- Cooldown periods prevent flapping.
- Scaling signals are leading (CPU, queue depth, p95 latency), not lagging (error rate).
15. Expected-load headroom
- Capacity at least 2× (team) / 3-5× (scalable) expected peak — load test should prove this.
- Cross-reference test-coverage findings on stress tests.
- Failure modes at over-capacity: graceful 503s > silent degradation > timeouts.
Severity classification
| Severity |
Meaning |
| critical |
Guaranteed failure at modest scale: N+1 on hot path, missing index on FK, single-instance stateful service. |
| high |
Will degrade badly within plausible growth window: lack of pagination, sync blocking in async runtime, shared cache missing for expensive compute. |
| medium |
Not immediate pain but will compound: suboptimal index, missing rate limit, missing connection pool tuning. |
| low |
Nice-to-have optimizations: compression, HTTP/2, prefetch, etc. |
| info |
Observations about current capacity / headroom. |
Output format
- id: SCALE-<NNN>
severity: ...
category: db-index | n-plus-1 | query | pool | cache | stateless | async | rate-limit | pagination | search | concurrency | geo | payload | autoscale | headroom
title: ...
location: <file:line or system-level>
description: |
<what, why, scale at which this becomes a problem>
evidence:
- <code snippet / EXPLAIN output / profile sample>
remediation:
plan_mode: |
<fix description, rough effort>
edit_mode: |
<diff / config change>
references:
- <database docs / performance guide>
blocker_at_tier: [...]
expected_impact: |
<e.g. "at 1k QPS, query time drops from 300ms p99 to ~5ms p99">
Dimension summary:
## Scalability Summary
Scope tier: <...>
Expected load: <if provided>
Current hot paths: <top 3 endpoints / queries by estimated load>
Top 3 scaling risks:
1. ...
Indexes: <count OK, count missing>
N+1 detected: <count>
Cacheable but uncached: <count>
Paginated endpoints: <X/Y>
Stateful components: <list>
Example findings
Example 1 — N+1 on list endpoint
- id: SCALE-003
severity: high
category: n-plus-1
title: "GET /api/projects fires N+1 queries loading owner per row"
location: "src/routes/projects.ts:19"
description: |
The handler fetches projects then iterates to resolve `owner` via a
separate lookup per row. Production logs show this endpoint issues
1 + N queries per request, where N averages 43 and peaks at 500+
for admin users. p99 latency of the endpoint is 1.4s in prod (SLO
is 400ms). The fix is trivial but the gap compounds as project
count grows — at scale, this endpoint is the single-largest source
of DB load.
evidence:
- |
// src/routes/projects.ts:19
const projects = await db.projects.findAll({ where: { org_id } });
for (const p of projects) {
p.owner = await db.users.findByPk(p.owner_id);
}
- "Postgres pg_stat_statements: top by calls is the user-by-pk query (SELECT from users WHERE id=$1), 18M calls/day."
remediation:
plan_mode: |
1. Use the ORM's eager-loading primitive:
`include: [{ model: User, as: 'owner' }]` (Sequelize),
`.preload(:owner)` (Ecto / Rails), `joinedload` / `selectinload`
(SQLAlchemy).
2. Add a test that asserts the endpoint issues bounded (≤2)
queries.
3. Audit sibling endpoints for the same pattern.
edit_mode: |
Safe. Diff adds eager-loading include + the query-count test.
references:
- "Martin Fowler — N+1 query problem"
expected_impact: "p99 1400ms → ~120ms; DB load on users reduced ~95%."
blocker_at_tier: [team, scalable]
Example 2 — Missing FK index causes hot-query seq scan
- id: SCALE-010
severity: high
category: db-index
title: "messages.conversation_id has no index — every thread fetch seq-scans"
location: "db/schema.sql:88"
description: |
`messages.conversation_id` has a foreign-key constraint but no
index. The thread-fetch query `SELECT ... FROM messages WHERE
conversation_id = $1 ORDER BY created_at DESC LIMIT 50` performs a
sequential scan on a 62M-row table. pg_stat_statements shows this
query contributing 22% of total DB time. An appropriate index
drops the query from ~300ms to ~2ms and vastly reduces cache
thrash.
evidence:
- |
-- db/schema.sql:88
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES conversations(id),
...
);
-- no index on conversation_id
- "EXPLAIN ANALYZE shows Seq Scan on messages, Rows Removed by Filter: 61,998,112"
remediation:
plan_mode: |
1. `CREATE INDEX CONCURRENTLY idx_messages_conv_created ON
messages (conversation_id, created_at DESC);` — matches the
ORDER BY so it can serve sorted reads without a filesort.
2. Verify with EXPLAIN ANALYZE after build.
3. Add an ORM-level contract test to prevent regression on the
thread endpoint (query count / plan shape).
edit_mode: |
Safe on Postgres with CONCURRENTLY (no table lock). Confirm
before applying — builds may take 15+ min on a 62M-row table,
and CI that runs migrations on deploy must allow this window.
references:
- "Postgres — Multicolumn Indexes"
expected_impact: "Thread fetch p99 300ms → 2ms."
blocker_at_tier: [team, scalable]
Example 3 — In-process session state prevents horizontal scaling
- id: SCALE-022
severity: critical
category: stateless
title: "Sessions stored in process memory — cannot run >1 instance"
location: "src/auth/session.ts:8"
description: |
Sessions are held in a module-level `Map<string, Session>` in the
application process. Any attempt to run a second instance behind
a load balancer breaks authentication because sessions live on a
single pod. Current deployment is 1 replica — which is also a
single point of failure, and prevents scaling beyond a single
node's capacity. The limit is implicit and invisible in metrics
until the day the team tries to scale.
evidence:
- |
// src/auth/session.ts:8
const sessions = new Map<string, Session>();
export function getSession(id: string) { return sessions.get(id); }
remediation:
plan_mode: |
1. Move sessions to a shared store: Redis (fastest), DB
(simplest), or signed JWTs (stateless).
2. For Redis: use `ioredis` + a session library
(`connect-redis`, custom wrapper). TTL matches session
lifetime.
3. Keep a thin in-process LRU for read-through caching, write-
through invalidated on mutations.
4. Update the deployment to >1 replica + enable rolling.
edit_mode: |
Architectural change. Requires explicit confirmation +
coordination with ops for Redis provisioning + session
migration (users online at cutover lose their session unless a
dual-read strategy is used).
references:
- "Twelve-Factor App — VI. Processes"
expected_impact: "Enables horizontal scale + HA."
blocker_at_tier: [team, scalable]
Edit-mode remediation
Safe:
- Adding missing indexes (use
CREATE INDEX CONCURRENTLY for Postgres on large tables — flag to user).
- Adding
LIMIT / pagination to list endpoints.
- Adding
prefetch_related / joinedload / DataLoader for N+1.
- Adding cache-control headers.
- Adding response compression middleware.
- Tuning connection pool sizes (within conservative bounds).
- Adding rate-limit middleware with defaults.
Require confirmation:
- Migrating from in-process state to a shared store.
- Restructuring for horizontal scaling.
- Introducing a job queue or cache layer (adds dependency).
- Changing sync → async paradigm.
- Adding read replicas / sharding / caching tiers (architectural).
- Changing autoscaling policy.
Do not
- Do not prescribe scalable-tier solutions (sharding, multi-region, event sourcing) to prototype-tier apps — complexity without justification kills small projects.
- Do not add indexes speculatively — each index has write + storage cost. Add them where queries justify them.
- Do not recommend caching as a fix for every slow query. First improve the query; cache what's still hot.
- Do not confuse "faster" with "more scalable" — some optimizations reduce latency without increasing throughput, and vice-versa.
- Do not propose microservices as a scalability fix for a monolith unless the actual bottleneck justifies it. Most "we need microservices" problems are really "we need indexes and a queue".
- Do not ignore the cost side. Scalability changes can 10× the infrastructure bill; surface the tradeoff.
1---2name: scalability-review3description: Scope-aware review of scalability and performance readiness — database indexing and query patterns, N+1 queries, caching strategy, connection pooling, horizontal scalability, statelessness, rate limiting, pagination, background job architecture, and expected-load headroom. Thresholds adapt sharply to the project's scope tier (prototype, team, scalable). Use when the user asks about "scalability", "performance", "will this scale", "horizontal scaling", "caching", invokes /scalability-review, or when the orchestrator delegates. Stack-agnostic, mode-aware.4license: Apache-2.05---67# Scalability Review89You review whether the application can handle its target load and grow with demand. Scalability is not a single property — it's the absence of several specific anti-patterns.1011**This skill is the most scope-tier-sensitive.** A prototype should not be forced to defend horizontal scaling. A scalable-tier system must.1213## Inputs1415From orchestrator: `scope_tier`, `stack_summary`, `gitnexus_indexed`, entry points, and any context on expected load (QPS, data volume, tenant count, geographic distribution).1617If expected load isn't known, ask — but keep it simple:1819```20Rough scale targets for scaling review:21- Concurrent users (peak): ?22- Requests/sec at peak: ?23- Total data volume (e.g. rows in largest table): ?24- Tenants / accounts: ?25- Geographic spread: single region / multi-region / global?26```2728## Mode detection2930- **Plan mode** — produce scalability report with prioritized findings.31- **Edit mode** — apply targeted fixes. Indexes, pagination, connection pool tuning, cache additions can usually be applied. Sharding, read replicas, architectural restructuring must be proposed and discussed, not applied.3233## Thresholds by tier3435| Tier | DB indexing | N+1 | Caching | Stateless | Rate limiting | Pagination | Async jobs | Headroom |36|---|---|---|---|---|---|---|---|---|37| prototype | advisory | advisory | optional | advisory | optional | required on list endpoints | optional | advisory |38| team | **required** on common queries | **required** to fix | recommended where beneficial | **required** (no in-process session state) | **required** on public + auth endpoints | **required** | **required** for long-running work | 2× expected load |39| scalable | **required + reviewed regularly** | **required** | **required** — multi-tier, invalidation strategy | **required + tested** | **required + per-tenant** | **required + cursor-based for large sets** | **required + queue-backed** | 3-5× expected load, headroom monitored |4041## Review surface4243### 1. Database schema and indexing4445- **Check for indexes on common access patterns.** Use GitNexus `mcp__gitnexus__query` if available to find all `WHERE`/`ORDER BY`/`JOIN` columns. Otherwise scan ORM queries / repository methods / SQL files.46- **For each frequently-queried table:**47 - Primary key exists and is appropriate (avoid UUID PKs on InnoDB if hot insert rate is high; use ULIDs or sortable UUIDs).48 - Foreign keys have matching indexes.49 - Composite indexes match query predicates (column order matters — most selective first).50 - Covering indexes for hot read paths where it meaningfully reduces I/O.51 - No duplicate / redundant indexes.52- **Anti-patterns:**53 - `SELECT *` in hot paths — fetches columns you don't need; blocks covering-index optimization.54 - `LIKE '%foo%'` on un-indexed text — full scan; use full-text search or GIN/GIST (Postgres).55 - `OR` across indexed + non-indexed columns causing index skip.56 - `ORDER BY RAND()` — full scan + filesort.57 - Queries without `LIMIT` returning large result sets.58- **Migrations:**59 - Adding an index on a large table without `CONCURRENTLY` (Postgres) blocks writes.60 - Adding NOT NULL columns with a default on a large table can rewrite the whole table (Postgres < 11).61 - Long-running migrations require online-migration patterns.6263### 2. N+1 queries6465- Grep ORM call patterns for loops that trigger per-iteration queries:66 - Rails: `.each { .foo.bar }` without `.includes(:foo)` / `.eager_load`.67 - Django: loop over `.all()` accessing related fields without `select_related` / `prefetch_related`.68 - SQLAlchemy: accessing relationships without `joinedload` / `selectinload`.69 - Prisma: `.findMany` then `.findUnique` in a loop.70 - ActiveRecord / similar: nested `.map` with DB access inside.71- Graphql resolvers without a DataLoader for the N+1 classic case.72- Check query logs (if captured in dev): same query repeated with different parameters → N+1 signature.7374### 3. Query analysis7576- Enable query logging in dev / staging to observe real query counts per request.77- For critical endpoints: run `EXPLAIN ANALYZE` (Postgres) / `EXPLAIN FORMAT=JSON` (MySQL). Findings if:78 - Sequential scans on large tables.79 - Sort operations without index support on `ORDER BY`.80 - Temporary tables / filesort.81 - Hash joins where nested-loop would be faster (or vice-versa).82- Slow query log enabled in production at team+ tier.8384### 4. Connection pooling8586- **DB connection pool** present and sized appropriately:87 - Pool size roughly: `(max_concurrent_requests × avg_queries_per_request × avg_query_time) / target_latency`. Too-small pools cause waiting; too-large pools exhaust DB.88 - At scalable tier, use a dedicated pooler (PgBouncer, Odyssey) between app and DB — especially for serverless / many-instance deployments.89 - Pool timeouts configured (wait timeout, idle timeout, max lifetime).90- **HTTP client pools** for outbound requests — avoid creating a new client per request.91- **Redis / cache pools** configured.9293### 5. Caching9495#### A. Tiered strategy9697- **Browser cache** (`Cache-Control`, ETag, Last-Modified) on static and semi-static responses.98- **CDN / edge cache** for cacheable assets and API responses.99- **Shared cache** (Redis / Memcached) for cross-instance data.100- **In-process cache** for very hot read-mostly data — with clear invalidation rules.101102#### B. What to cache103104- Read-heavy queries with stable results.105- External API responses (rate-limited APIs especially).106- Computed views (denormalized aggregates).107- Per-request memoization of repeated lookups.108109#### C. Invalidation110111- **Cache invalidation strategy documented** — or cache drift / stale data becomes a bug source.112- Write-through / write-behind / TTL-only — which does the app use per cache? Is it appropriate to the data's staleness tolerance?113- Cache stampede prevention: `singleflight`, lock-on-miss, probabilistic early expiration.114115#### D. Anti-patterns116117- Caching user-specific data in a shared cache without tenant / user segmentation (cross-tenant data leak).118- `TTL = Infinity` without invalidation trigger.119- Caching error responses.120121### 6. Statelessness / horizontal scaling122123- **In-process session state** is a finding at team+ tier. Sessions in a shared store (Redis, DB, signed JWTs).124- **In-process caches with local-only invalidation** don't scale — adding a second instance causes divergence.125- **Sticky sessions** as a workaround for stateful design — flag.126- **WebSocket / SSE / long-poll connections** on an auto-scaled service — scaling requires a backplane (Redis pub/sub, message queue, managed service like Pusher / Ably / API Gateway WebSockets).127- **Singleton state** (in-process rate limiter, in-process scheduler, in-process job queue) that must be coordinated across instances.128- **Container startup time** — slow cold-start makes scaling reactive rather than elastic.129130### 7. Background work and async131132- **Long-running work on the request path** is a finding:133 - Report generation, bulk operations, third-party API chains — move to background.134- **Job queue in use:** BullMQ, Sidekiq, Celery, RQ, SQS, GCP Tasks, Kafka, Temporal — any?135- **Queue properties:**136 - Persistent (durable).137 - At-least-once delivery + consumer idempotency.138 - Dead-letter queue for unprocessable messages.139 - Monitored for backlog growth.140 - Autoscaling based on queue depth.141- **Cron / scheduled jobs:**142 - Use a distributed scheduler (not per-instance cron) — avoid duplicate executions.143 - Long-running cron jobs can overlap — detect and prevent.144145### 8. Rate limiting146147Overlaps with security-audit + reliability-audit. Scalability angle:148149- Rate limits prevent a noisy tenant / bot / runaway-loop from degrading service for everyone.150- Per-API-key / per-IP / per-user limits at team+ tier.151- Per-tenant / per-endpoint differentiated limits at scalable tier.152- Enforced at the edge (reverse proxy, API gateway) preferred over application-layer for DDoS absorption.153- Quota + bucket semantics documented (burst, refill rate).154- 429 responses include `Retry-After` header.155156### 9. Pagination157158- Every list endpoint must paginate at team+ tier.159- **Cursor-based pagination** for large or frequently-updated datasets (avoids offset drift + offset performance).160- Offset pagination acceptable for small bounded lists only.161- Max page size enforced on the server — client can't request 100k records.162- Total count queries are expensive on large tables — avoid or cache.163164### 10. Search and large scans165166- Avoid full-table scans in user-facing paths.167- Full-text search delegated to a dedicated system (Elasticsearch / OpenSearch / Meilisearch / Typesense / Postgres FTS with proper indexes) rather than `ILIKE '%...%'` at scale.168- Aggregate / reporting queries isolated from OLTP DB (read replica, data warehouse, OLAP store) at scalable tier.169170### 11. Concurrency + throughput171172- **Request-handling concurrency model** appropriate to stack:173 - Node.js: I/O-bound workloads benefit from high async concurrency, single thread per event loop — worker threads / cluster for CPU.174 - Python: GIL-bound — use multiple processes (gunicorn workers, uvicorn workers) for CPU-bound; asyncio for I/O-bound.175 - Go: default is efficient; watch for goroutine leaks.176 - JVM: thread pool sizing critical — one-thread-per-request doesn't scale past pool limit.177- **Blocking I/O on async runtimes** is a finding — sync DB driver in an async event loop starves the loop.178179### 12. Geographic distribution (scalable tier)180181- Multi-region active-active or active-passive?182- Latency sensitivity: where do users live vs. where's the data?183- CDN + edge compute for global reads.184- Database replication — primary / replica / multi-primary? Read-your-writes consistency semantics?185- Data sovereignty constraints from compliance-check (some data can't cross borders).186187### 13. Payload sizes188189- Response sizes capped or streamed. Multi-megabyte JSON responses are a finding.190- Gzip / brotli compression enabled.191- Images served with appropriate format (WebP / AVIF) + size variants.192193### 14. Capacity planning + autoscaling194195At team+ tier:196197- Autoscaling configured with sensible thresholds.198- Min instance count > 1 for availability.199- Scale-up is **faster** than scale-down (avoid oscillation; gentler scale-down).200- Cooldown periods prevent flapping.201- Scaling signals are **leading** (CPU, queue depth, p95 latency), not lagging (error rate).202203### 15. Expected-load headroom204205- Capacity at least 2× (team) / 3-5× (scalable) expected peak — load test should prove this.206- Cross-reference test-coverage findings on stress tests.207- Failure modes at over-capacity: graceful 503s > silent degradation > timeouts.208209## Severity classification210211| Severity | Meaning |212|---|---|213| critical | Guaranteed failure at modest scale: N+1 on hot path, missing index on FK, single-instance stateful service. |214| high | Will degrade badly within plausible growth window: lack of pagination, sync blocking in async runtime, shared cache missing for expensive compute. |215| medium | Not immediate pain but will compound: suboptimal index, missing rate limit, missing connection pool tuning. |216| low | Nice-to-have optimizations: compression, HTTP/2, prefetch, etc. |217| info | Observations about current capacity / headroom. |218219## Output format220221```yaml222- id: SCALE-<NNN>223 severity: ...224 category: db-index | n-plus-1 | query | pool | cache | stateless | async | rate-limit | pagination | search | concurrency | geo | payload | autoscale | headroom225 title: ...226 location: <file:line or system-level>227 description: |228 <what, why, scale at which this becomes a problem>229 evidence:230 - <code snippet / EXPLAIN output / profile sample>231 remediation:232 plan_mode: |233 <fix description, rough effort>234 edit_mode: |235 <diff / config change>236 references:237 - <database docs / performance guide>238 blocker_at_tier: [...]239 expected_impact: |240 <e.g. "at 1k QPS, query time drops from 300ms p99 to ~5ms p99">241```242243Dimension summary:244245```markdown246## Scalability Summary247248Scope tier: <...>249Expected load: <if provided>250Current hot paths: <top 3 endpoints / queries by estimated load>251252Top 3 scaling risks:253 1. ...254255Indexes: <count OK, count missing>256N+1 detected: <count>257Cacheable but uncached: <count>258Paginated endpoints: <X/Y>259Stateful components: <list>260```261262## Example findings263264### Example 1 — N+1 on list endpoint265266```yaml267- id: SCALE-003268 severity: high269 category: n-plus-1270 title: "GET /api/projects fires N+1 queries loading owner per row"271 location: "src/routes/projects.ts:19"272 description: |273 The handler fetches projects then iterates to resolve `owner` via a274 separate lookup per row. Production logs show this endpoint issues275 1 + N queries per request, where N averages 43 and peaks at 500+276 for admin users. p99 latency of the endpoint is 1.4s in prod (SLO277 is 400ms). The fix is trivial but the gap compounds as project278 count grows — at scale, this endpoint is the single-largest source279 of DB load.280 evidence:281 - |282 // src/routes/projects.ts:19283 const projects = await db.projects.findAll({ where: { org_id } });284 for (const p of projects) {285 p.owner = await db.users.findByPk(p.owner_id);286 }287 - "Postgres pg_stat_statements: top by calls is the user-by-pk query (SELECT from users WHERE id=$1), 18M calls/day."288 remediation:289 plan_mode: |290 1. Use the ORM's eager-loading primitive:291 `include: [{ model: User, as: 'owner' }]` (Sequelize),292 `.preload(:owner)` (Ecto / Rails), `joinedload` / `selectinload`293 (SQLAlchemy).294 2. Add a test that asserts the endpoint issues bounded (≤2)295 queries.296 3. Audit sibling endpoints for the same pattern.297 edit_mode: |298 Safe. Diff adds eager-loading include + the query-count test.299 references:300 - "Martin Fowler — N+1 query problem"301 expected_impact: "p99 1400ms → ~120ms; DB load on users reduced ~95%."302 blocker_at_tier: [team, scalable]303```304305### Example 2 — Missing FK index causes hot-query seq scan306307```yaml308- id: SCALE-010309 severity: high310 category: db-index311 title: "messages.conversation_id has no index — every thread fetch seq-scans"312 location: "db/schema.sql:88"313 description: |314 `messages.conversation_id` has a foreign-key constraint but no315 index. The thread-fetch query `SELECT ... FROM messages WHERE316 conversation_id = $1 ORDER BY created_at DESC LIMIT 50` performs a317 sequential scan on a 62M-row table. pg_stat_statements shows this318 query contributing 22% of total DB time. An appropriate index319 drops the query from ~300ms to ~2ms and vastly reduces cache320 thrash.321 evidence:322 - |323 -- db/schema.sql:88324 CREATE TABLE messages (325 id BIGSERIAL PRIMARY KEY,326 conversation_id BIGINT NOT NULL REFERENCES conversations(id),327 ...328 );329 -- no index on conversation_id330 - "EXPLAIN ANALYZE shows Seq Scan on messages, Rows Removed by Filter: 61,998,112"331 remediation:332 plan_mode: |333 1. `CREATE INDEX CONCURRENTLY idx_messages_conv_created ON334 messages (conversation_id, created_at DESC);` — matches the335 ORDER BY so it can serve sorted reads without a filesort.336 2. Verify with EXPLAIN ANALYZE after build.337 3. Add an ORM-level contract test to prevent regression on the338 thread endpoint (query count / plan shape).339 edit_mode: |340 Safe on Postgres with CONCURRENTLY (no table lock). Confirm341 before applying — builds may take 15+ min on a 62M-row table,342 and CI that runs migrations on deploy must allow this window.343 references:344 - "Postgres — Multicolumn Indexes"345 expected_impact: "Thread fetch p99 300ms → 2ms."346 blocker_at_tier: [team, scalable]347```348349### Example 3 — In-process session state prevents horizontal scaling350351```yaml352- id: SCALE-022353 severity: critical354 category: stateless355 title: "Sessions stored in process memory — cannot run >1 instance"356 location: "src/auth/session.ts:8"357 description: |358 Sessions are held in a module-level `Map<string, Session>` in the359 application process. Any attempt to run a second instance behind360 a load balancer breaks authentication because sessions live on a361 single pod. Current deployment is 1 replica — which is also a362 single point of failure, and prevents scaling beyond a single363 node's capacity. The limit is implicit and invisible in metrics364 until the day the team tries to scale.365 evidence:366 - |367 // src/auth/session.ts:8368 const sessions = new Map<string, Session>();369 export function getSession(id: string) { return sessions.get(id); }370 remediation:371 plan_mode: |372 1. Move sessions to a shared store: Redis (fastest), DB373 (simplest), or signed JWTs (stateless).374 2. For Redis: use `ioredis` + a session library375 (`connect-redis`, custom wrapper). TTL matches session376 lifetime.377 3. Keep a thin in-process LRU for read-through caching, write-378 through invalidated on mutations.379 4. Update the deployment to >1 replica + enable rolling.380 edit_mode: |381 Architectural change. Requires explicit confirmation +382 coordination with ops for Redis provisioning + session383 migration (users online at cutover lose their session unless a384 dual-read strategy is used).385 references:386 - "Twelve-Factor App — VI. Processes"387 expected_impact: "Enables horizontal scale + HA."388 blocker_at_tier: [team, scalable]389```390391## Edit-mode remediation392393Safe:394- Adding missing indexes (use `CREATE INDEX CONCURRENTLY` for Postgres on large tables — flag to user).395- Adding `LIMIT` / pagination to list endpoints.396- Adding `prefetch_related` / `joinedload` / DataLoader for N+1.397- Adding cache-control headers.398- Adding response compression middleware.399- Tuning connection pool sizes (within conservative bounds).400- Adding rate-limit middleware with defaults.401402Require confirmation:403- Migrating from in-process state to a shared store.404- Restructuring for horizontal scaling.405- Introducing a job queue or cache layer (adds dependency).406- Changing sync → async paradigm.407- Adding read replicas / sharding / caching tiers (architectural).408- Changing autoscaling policy.409410## Do not411412- Do not prescribe scalable-tier solutions (sharding, multi-region, event sourcing) to prototype-tier apps — complexity without justification kills small projects.413- Do not add indexes speculatively — each index has write + storage cost. Add them where queries justify them.414- Do not recommend caching as a fix for every slow query. First improve the query; cache what's still hot.415- Do not confuse "faster" with "more scalable" — some optimizations reduce latency without increasing throughput, and vice-versa.416- Do not propose microservices as a scalability fix for a monolith unless the actual bottleneck justifies it. Most "we need microservices" problems are really "we need indexes and a queue".417- Do not ignore the cost side. Scalability changes can 10× the infrastructure bill; surface the tradeoff.