The Tech Stack Choice

VvW runs on FastAPI (Python async web framework) with SQLAlchemy async ORM connecting to PostgreSQL in production (SQLite in development). Redis handles sessions, cooldowns, rate limiting, and short-lived caches. APScheduler manages background jobs (season resets, daily quest generation, leaderboard snapshots).

Why FastAPI over Django or Flask? Pure async from the ground up. A single FastAPI worker can handle hundreds of concurrent requests without blocking. For a game where every player action is an API call, this matters enormously.

Why Async Matters for MMOs

In a synchronous web framework, a request that does 3 database queries takes ~15ms per query = ~45ms total, and blocks a worker thread the entire time. In an async framework, those 3 queries happen concurrently — total time drops to ~15ms, and the worker is freed to handle other requests while waiting for I/O.

For a game with 1,000 concurrent players each making 1 request per second, this difference is the gap between a server that handles the load and one that queues indefinitely. VvW uses async def on every endpoint and await on every database call.

Redis Caching Strategy

Redis sits between FastAPI and PostgreSQL for every hot-path read:

Cache TypeTTLWhat's Cached
Static JSON (lru_cache)Forever (process life)items.json, monsters.json, locations.json
Leaderboards5 minutesTop 20 per category
Character stats60 secondsDerived stat calculations
Clan info5 minutesClan name, level, member count
World Boss HP10 secondsCurrent HP (hot during raids)
CooldownsPer action (1h–24h)dungeon/npc/mission last timestamps

The most important cached object is static JSON. Loading items.json (921 items, ~450KB) from disk on every item request would be catastrophic. Python's @lru_cache loads it once at process start and serves it from memory for the process lifetime.

Database Optimization

Every foreign key column has an index. Every commonly-queried column has an index. The most-hit query pattern — "get character by ID, then get their inventory" — hits indexes on both tables and returns in under 2ms even with 100k rows.

We use SQLAlchemy's selectinload for relationship loading instead of lazy loading. Lazy loading in a async context causes the N+1 query problem. selectinload fires two queries total instead of N+1.

KEY OPTIMIZATION

The single biggest performance improvement was adding a composite index on (character_id, item_id) for inventory queries. Inventory reads dropped from 12ms to 0.8ms after this index was added.

Horizontal Scaling Plan

The path from 1 server to 10 servers is already designed:

  • Phase 1 (current): Single Hetzner VPS, nginx as reverse proxy, 4 FastAPI workers via Uvicorn
  • Phase 2 (1k players): Add read replica PostgreSQL. Route all GET requests to replica, only writes to primary
  • Phase 3 (5k players): Separate Redis to dedicated instance. Add 2nd app server behind load balancer. Sticky sessions via Redis (not in-memory)
  • Phase 4 (10k+ players): PostgreSQL connection pooling via PgBouncer. CDN for all static assets including the 1,426 programmatic pages. WebSocket server separation

The key architectural decision that makes this possible: no local state in FastAPI workers. Every piece of state lives in PostgreSQL or Redis. This means any worker can handle any request — perfect for horizontal scaling.

Load Test Results

We simulated 500 concurrent users using Locust, each performing a realistic session: login → view dashboard → 5 hunts → check inventory → claim daily mission → logout.

MetricResultTarget
Requests/second847 req/s500 req/s
p50 response time28ms<100ms
p95 response time112ms<300ms
p99 response time289ms<500ms
Error rate0.02%<0.1%
DB connection pool exhaustion0 events0 events

The bottleneck at 500 concurrent users was the PostgreSQL connection pool limit (20 connections). We increased it to 50 and retested — p95 dropped to 87ms. Phase 2 scaling with PgBouncer will address this permanently.

CDN for Programmatic Pages

Our 1,426 programmatic SEO pages (items, monsters, locations) are ideal CDN candidates — they're static HTML generated from JSON data. On a CDN, each page serves from a regional edge node in under 20ms globally. We serve these from nginx-cached static files in Phase 1 and will move to a CDN in Phase 3.

Built to Scale With You

VvW's architecture was designed for the long term. Play today and experience a game built to handle whatever growth comes.

Join the Beta →