Backends¶
A backend is where waygate stores route state and the audit log. Swapping backends requires a one-line change; everything else (decorators, middleware, CLI, audit log) works unchanged.
Choosing a backend¶
| Backend | Persistence | Multi-instance | Best for |
|---|---|---|---|
MemoryBackend |
No | No | Development, testing |
FileBackend |
Yes | No (single process) | Simple single-instance deployments |
RedisBackend |
Yes | Yes | Production, load-balanced |
| Custom | You decide | You decide | Any other storage layer |
MemoryBackend (default)¶
State lives in a Python dict. Lost on restart. The CLI cannot share state with the running server unless it also uses the in-process engine (e.g. via the admin API).
from waygate import MemoryBackend
from waygate import WaygateEngine
engine = WaygateEngine(backend=MemoryBackend())
Best for: development, unit tests, demos.
FileBackend¶
State is written to a JSON file on disk. The CLI and the running server share state as long as both point to the same file.
from waygate import FileBackend
from waygate import WaygateEngine
engine = WaygateEngine(backend=FileBackend(path="waygate-state.json"))
Or via environment variables:
File format:
{
"states": {
"GET:/payments": { "path": "GET:/payments", "status": "maintenance", ... }
},
"audit": [...]
}
Best for: single-instance deployments, CLI-driven workflows.
RedisBackend¶
State is stored in Redis. All instances in a deployment share the same state. Pub/sub keeps the dashboard SSE feed live across instances.
from waygate import RedisBackend
from waygate import WaygateEngine
engine = WaygateEngine(backend=RedisBackend(url="redis://localhost:6379/0"))
Or via environment variable:
Redis key schema:
| Key | Type | Description |
|---|---|---|
waygate:state:{path} |
String | JSON-serialised RouteState |
waygate:audit |
List | JSON-serialised AuditEntry items (capped at 1000) |
waygate:global |
String | JSON-serialised global maintenance config |
waygate:changes |
Pub/sub channel | Publishes on every set_state — used by SSE |
Best for: multi-instance / load-balanced production deployments.
Deploy Redis in the same region as your app
Every request runs at least one Redis read (engine.check()) and, when rate limiting
is active, an additional Redis write. If your Redis instance is in a different region
from your web service, each of those operations crosses a long-haul network link and
adds latency to every request. Always provision Redis in the same region as the
service that uses it.
Waygate Server + WaygateSDK (multi-service)¶
When you run multiple independent services, a dedicated Waygate Server acts as the centralised control plane. Each service connects to it via WaygateSDK, which keeps an in-process cache synced over a persistent SSE connection — so enforcement never touches the network per request.
graph TD
subgraph server["Waygate Server • port 9000"]
SS["WaygateServer(backend=...)\nDashboard · REST API · SSE"]
end
SS -->|HTTP + SSE| P
SS -->|HTTP + SSE| O
subgraph P["payments-app"]
PS["WaygateSDK\nlocal cache"]
end
subgraph O["orders-app"]
OS["WaygateSDK\nlocal cache"]
end
Waygate Server setup:
from waygate.server import WaygateServer
from waygate import MemoryBackend
waygate_app = WaygateServer(
backend=MemoryBackend(),
auth=("admin", "secret"),
token_expiry=3600, # dashboard / CLI users: 1 hour
sdk_token_expiry=31536000, # SDK service tokens: 1 year (default)
)
# Run: uvicorn myapp:waygate_app --port 9000
Service setup — three auth configurations:
from waygate.sdk import WaygateSDK
import os
# No auth on the Waygate Server — nothing needed
sdk = WaygateSDK(server_url="http://waygate-server:9000", app_id="payments-service")
# Auto-login (recommended for production): SDK logs in on startup with platform="sdk"
sdk = WaygateSDK(
server_url=os.environ["WAYGATE_SERVER_URL"],
app_id="payments-service",
username=os.environ["WAYGATE_USERNAME"],
password=os.environ["WAYGATE_PASSWORD"],
)
# Pre-issued token: obtain once via `waygate login`, store as a secret
sdk = WaygateSDK(
server_url=os.environ["WAYGATE_SERVER_URL"],
app_id="payments-service",
token=os.environ["WAYGATE_TOKEN"],
)
sdk.attach(app) # wires middleware + startup/shutdown
Which backend should the Waygate Server use?¶
| Waygate Server instances | Backend choice |
|---|---|
| 1 (development) | MemoryBackend — state lives in-process, lost on restart |
| 1 (production) | FileBackend — state survives restarts |
| 2+ (HA / load-balanced) | RedisBackend — all Waygate Server nodes share state via pub/sub |
Shared rate limit counters across SDK replicas¶
Each SDK client enforces rate limits locally using its own counters. When a service runs multiple replicas, each replica has independent counters — a 100/minute limit is enforced independently per replica by default.
To enforce the limit across all replicas combined, pass a shared RedisBackend as rate_limit_backend:
from waygate import RedisBackend
sdk = WaygateSDK(
server_url="http://waygate-server:9000",
app_id="payments-service",
rate_limit_backend=RedisBackend(url="redis://redis:6379/1"),
)
Deployment matrix¶
| Services | Replicas per service | Waygate Server backend | SDK rate_limit_backend |
|---|---|---|---|
| 1 | 1 | any — use embedded WaygateAdmin instead |
— |
| 2+ | 1 each | MemoryBackend or FileBackend |
not needed |
| 2+ | 2+ each | RedisBackend |
RedisBackend |
See Waygate Server guide → for a complete walkthrough.
Using make_engine (recommended)¶
make_engine() reads WAYGATE_BACKEND (and related env vars) so you never hardcode the backend:
from waygate import make_engine
engine = make_engine() # reads env + .waygate file
engine = make_engine(current_env="staging") # override env
engine = make_engine(backend="redis") # force backend type
This lets you use MemoryBackend locally and RedisBackend in production without touching your app code.
Custom backends¶
Any storage layer can be used by subclassing WaygateBackend:
from waygate import WaygateBackend
from waygate import AuditEntry, RouteState
class MyBackend(WaygateBackend):
async def get_state(self, path: str) -> RouteState:
# MUST raise KeyError if not found
...
async def set_state(self, path: str, state: RouteState) -> None:
...
async def delete_state(self, path: str) -> None:
...
async def list_states(self) -> list[RouteState]:
...
async def write_audit(self, entry: AuditEntry) -> None:
...
async def get_audit_log(
self, path: str | None = None, limit: int = 100
) -> list[AuditEntry]:
...
See Adapters: Building your own backend → for a full SQLite example.
Storage latency affects every request
waygate calls your backend on every incoming request. If your storage layer is remote (PostgreSQL, SQLite over NFS, a hosted database), the round-trip time to that storage is added to every request that passes through the middleware. Keep your storage instance in the same data centre or region as your application. The same applies to rate limit storage — a slow counter increment slows down every request that has a rate limit applied.
Lifecycle hooks¶
Override startup() and shutdown() for connection setup/teardown:
class MyBackend(WaygateBackend):
async def startup(self) -> None:
self._conn = await connect_to_db()
async def shutdown(self) -> None:
await self._conn.close()
Use async with engine: in your app lifespan to call them automatically:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
async with engine: # → backend.startup() … backend.shutdown()
yield
app = FastAPI(lifespan=lifespan)
Rate limit storage¶
Rate limit counters live separately from route state. The storage is auto-selected based on your main backend — you do not need to configure it separately.
| Backend | Rate limit storage | Multi-worker safe |
|---|---|---|
MemoryBackend |
In-process MemoryRateLimitStorage |
No |
FileBackend |
In-memory counters with periodic snapshot (FileRateLimitStorage) |
No |
RedisBackend |
Atomic Redis counters (RedisRateLimitStorage) |
Yes |
For production deployments with multiple workers, use RedisBackend. Redis counters are atomic and shared across all processes.
# Rate limit counters automatically use Redis when the main backend is Redis
engine = WaygateEngine(backend=RedisBackend("redis://localhost:6379/0"))
FileBackend and multi-worker
FileRateLimitStorage uses in-memory counters. Each worker process maintains its own independent counter, so the effective limit per client is limit * num_workers. Use RedisBackend for any deployment with more than one worker.