Handling Partial Failures in Fleet-Wide Migrations
The fleet rollout finished with 2,951 tenants migrated and 49 failed. Eleven failed on lock timeouts, twenty-three on a unique violation during an index build, nine because their schema was missing a column an older migration should have added, and six with connection errors during a network blip. The next deploy is in two days and depends on the new column existing everywhere. In a single-database world, a failed migration is binary; in a fleet, partial failure is the normal outcome of any large rollout, and the job is to converge the stragglers without blocking everyone else — and without letting the application assume a version the fleet has not reached. This guide classifies failures, retries the transient ones, quarantines the rest for individual repair, and keeps code compatible with version skew until the fleet converges. It belongs to Migrating Multi-Tenant Databases.
Symptom / Error Signatures
Partial failures show up in the orchestrator’s status table and in per-tenant logs:
tenant_0917 failed ERROR: canceling statement due to lock timeout -- transient
tenant_1204 failed ERROR: could not create unique index "invoices_number_key" ... is duplicated -- data
tenant_2230 failed ERROR: column "region" of relation "invoices" does not exist -- drift
tenant_3011 failed FATAL: terminating connection due to administrator command -- transient
The downstream symptom, if nothing is done, is application errors on straggler tenants after the next release assumes the new schema: column "tax_region" does not exist on exactly the 49 tenants that failed.
Root Cause Analysis
Across thousands of tenants, rare conditions become certainties: some tenant is busy when its migration runs, some tenant has duplicate data a new constraint rejects, some tenant missed an earlier migration because of an old incident, some connection drops. A fleet orchestrator should expect a small failure rate and treat each class differently:
| Failure class | Examples | Automatic action | Human action |
|---|---|---|---|
| transient | lock timeout, deadlock, connection reset | retry with backoff, up to N attempts | none unless persistent |
| data | unique violation, check violation, cast failure | quarantine | clean or migrate data, then retry |
| drift | missing column, unexpected object, wrong version history | quarantine | reconcile schema to baseline, then retry |
| migration bug | fails on every tenant with a feature | stop the wave | fix the migration |
The application side matters as much. While stragglers remain, the fleet spans versions, so code must work on the oldest version still present. The safest rule is to derive the fleet’s minimum version from the status table and gate any code that needs a newer schema on either that minimum or on the individual tenant’s version.
Immediate Mitigation
1. Classify the failures. Group failed tenants by error pattern from the status table.
-- PostgreSQL · registry · read-only · failures grouped by error signature
SELECT regexp_replace(last_error, '"[^"]*"', '"…"', 'g') AS error_pattern, count(*)
FROM tenant_migrations
WHERE status = 'failed'
GROUP BY 1 ORDER BY 2 DESC;
2. Retry the transient ones with backoff; they usually succeed.
-- PostgreSQL · registry · requeue transient failures
UPDATE tenant_migrations SET status = 'pending'
WHERE status = 'failed'
AND (last_error ILIKE '%lock timeout%' OR last_error ILIKE '%deadlock%' OR last_error ILIKE '%connection%');
3. Quarantine the rest so the orchestrator stops retrying them, and create a ticket per class with the affected tenants.
-- PostgreSQL · registry
UPDATE tenant_migrations SET status = 'quarantined' WHERE status = 'failed';
4. Protect the next release. Before deploying code that needs the new schema, check the fleet minimum; if stragglers remain, keep the code path gated.
-- PostgreSQL · registry · lowest version any active tenant is on
SELECT min(current_version) AS fleet_min FROM tenants WHERE active;
Permanent Fix / Long-Term Pattern
Build failure handling into the orchestrator. Every tenant attempt records the error; transient errors trigger automatic retries with exponential backoff and a cap; other errors move the tenant to quarantine with a notification. A daily report lists quarantined tenants and their age, so stragglers cannot be forgotten. Deploy tooling refuses to ship code that requires a schema version above the fleet minimum unless the code is gated per tenant.
Reduce each class at the source. Transient failures shrink with short lock timeouts plus retries, and with migrations scheduled per tenant outside their busy hours. Data failures shrink with a pre-flight check that runs the migration’s preconditions (duplicate checks, constraint checks) across the fleet before the rollout, turning a failure into a to-do list. Drift failures shrink with regular reconciliation of every tenant’s schema against the expected baseline, as in tracking schema versions across thousands of tenants. Make every tenant migration idempotent so retries are always safe, per Idempotent Script Design.
-- PostgreSQL · pre-flight precondition check run on every tenant before the rollout
-- tenants returning rows need data cleanup before the unique index can be built
SELECT number, count(*) FROM invoices GROUP BY number HAVING count(*) > 1 LIMIT 5;
Communicate with affected customers when a failure is theirs to see. A quarantined tenant usually experiences nothing — it simply stays on the previous version, which the application still supports — but if the fix requires cleaning their data (merging duplicates, correcting invalid values), the change may be visible to them. Agree a process with support for tenant-specific data repairs, including a record of what was changed and why, so a migration’s data cleanup never surprises a customer.
Verification Checklist
Frequently Asked Questions
Should one failed tenant stop the whole rollout? No, unless failures indicate a bug in the migration itself — for example, every tenant with a certain feature failing. Isolated failures should be retried or quarantined while the rest of the fleet continues.
How do I know when the rollout is really done? When the minimum schema version across all active tenants equals the target. Track it from the status table and verify it against the tenants’ own history tables.
Is it safe to retry a failed tenant migration? If the migration is transactional (PostgreSQL DDL) or idempotent, yes. Non-transactional steps, such as concurrent index builds, need idempotent guards and invalid-index cleanup before a retry.
How many retries are reasonable for transient failures? Three to five, with exponential backoff spanning minutes. A tenant that keeps timing out on locks is usually busy at that time of day; reschedule it for its quiet hours rather than retrying immediately.
What causes schema drift between tenants? Past partial failures that were never repaired, manual fixes on individual tenants, tenants restored from old backups, or tenants created from an outdated template. Regular reconciliation against the expected baseline catches them before the next rollout.