Canarying Schema Changes on a Subset of Tenants
The migration added a column and an index to every tenant’s invoices table and passed every test. It ran across all 3,000 tenants in forty minutes. The next morning, the 60 tenants on the legacy billing plan could not issue invoices, because their data had a pattern the new constraint rejected — something no test fixture contained. Fleets have a structural advantage single databases lack: you can try a change on a few tenants and watch before touching the rest. A canary wave turns “tested on fixtures” into “proven on real tenants with real data and real traffic”, and a health gate between waves turns a fleet-wide incident into a small, reversible one. This guide designs canary groups, defines the gate, and connects it to the orchestrator. It belongs to Migrating Multi-Tenant Databases.
Symptom / Error Signatures
You need canaries if fleet migrations have ever caused:
- A problem that affected every tenant at once, discovered only after the rollout completed.
- Failures limited to a subset of tenants with unusual data, a legacy feature, or a very large dataset — invisible in staging.
- Performance regressions on tenants of a particular size, for example a new index that is harmless on small tenants and slow to build or to maintain on large ones.
A canary gate that works reports something like: wave 1 (32 tenants): error rate 0.41% vs control 0.39%, p99 212 ms vs 205 ms — PASS, or FAIL: invoice creation errors 4.8% vs 0.2% on 3 canary tenants (plan: legacy_billing).
Root Cause Analysis
Tests and staging use fixtures and a handful of synthetic tenants. Production tenants differ in size, feature usage, data history and traffic, and migrations interact with all four: a backfill’s duration depends on data volume, a constraint’s validation depends on historical data quality, a new index’s cost depends on write rate. The only reliable way to learn how a migration behaves across that diversity is to apply it to a sample of real tenants and observe.
Two design choices make the observation meaningful. The canary group must be representative: stratified across plans, sizes, regions and feature flags, including at least one of the largest tenants. And the gate must compare the canary with a control group of not-yet-migrated tenants over the same period, so ordinary fluctuations (a traffic spike, an unrelated incident) affect both and cancel out.
| Canary selection | What it catches | What it misses |
|---|---|---|
| internal/test tenants only | syntax, obvious errors | real data, scale |
| random 1% | common data patterns | rare plans, largest tenants |
| stratified 1% + largest tenants | most data patterns, scale effects | extremely rare edge cases |
| customer-opted “early adopters” | real usage with tolerance | may not be representative |
Immediate Mitigation
If a fleet migration without canaries is causing problems now:
1. Stop the rollout. Mark remaining tenants hold in the orchestrator’s status table so no further tenants are migrated.
-- PostgreSQL · registry · pause the fleet
UPDATE tenant_migrations SET status = 'hold' WHERE status = 'pending';
2. Find the pattern among affected tenants. Join error data to tenant attributes (plan, size, features, region) to see what the failing tenants have in common.
3. Fix forward or roll back only the affected tenants, then resume the rollout with a canary wave that deliberately includes tenants with the problematic attribute.
Permanent Fix / Long-Term Pattern
Make waves and gates part of the orchestrator, not a manual habit.
1. Assign waves automatically from the tenant registry: wave 0 for internal tenants, wave 1 for a stratified 1–2% sample plus the largest tenants, wave 2 for 10–20%, wave 3 for the rest. Re-draw the canary for each rollout so the same customers are not always first.
-- PostgreSQL · registry · stratified canary: ~1% per plan/size stratum plus the 3 largest tenants
WITH ranked AS (
SELECT tenant_id, plan, size_band,
row_number() OVER (PARTITION BY plan, size_band ORDER BY random()) AS rn,
count(*) OVER (PARTITION BY plan, size_band) AS n
FROM tenants WHERE internal = false
)
UPDATE tenant_migrations tm SET wave = 1
FROM ranked r
WHERE tm.tenant_id = r.tenant_id
AND (r.rn <= greatest(1, ceil(r.n * 0.01))
OR r.tenant_id IN (SELECT tenant_id FROM tenants ORDER BY data_bytes DESC LIMIT 3));
2. Define the gate as a query over metrics you already collect: error rate for the key operations, p95/p99 latency, database load attributable to the canary tenants — each compared with the control group over a soak period long enough to include the tenants’ normal activity (often a few hours; a full business day for billing-type features).
3. Hold on failure, automatically. A failed gate sets remaining waves to hold and pages the owner; a pass promotes the next wave. Record gate results with the rollout for later review.
Canarying works only if the application tolerates a mixed fleet, which requires backward-compatible migrations — the discipline in Expand and Contract Methodology. The same wave structure is useful for feature enablement after the schema is in place, combining with progressive schema rollout with percentage flags.
Verification Checklist
Frequently Asked Questions
How large should the canary be? Large enough to be representative — usually 1–2% of tenants, stratified by plan, size and features, plus the largest tenants — and small enough that a failure affects few customers.
How long should the canary soak? Long enough to exercise the code paths the migration affects. For migrations touching frequently used tables, a few hours is often enough; for monthly or daily processes such as billing, soak through at least one run of those processes.
Should the largest tenants go first or last? Include at least one in the canary to surface scale problems early, then decide per migration: many teams run the remaining largest tenants in a dedicated wave with extra monitoring.
Can canaries work for shared-table designs? Not for the DDL itself, which applies to all tenants at once. They still work for behaviour: gate the new code path by tenant (a feature flag keyed by tenant) and roll it out in waves after the backward-compatible schema change.