
Resilience
Disaster Recovery vs Business Continuity: Where the Line Sits
A disaster recovery and business continuity plan splits cleanly in two: DR restores a named system under RTO, BC sustains the process it serves under MTD.
Search for a disaster recovery and business continuity plan and you are offered one template with a DR chapter inside it. That shape is an artefact of how the documents are sold, not of how the work divides. The two halves have different owners, different units of measure, different audiences and different evidence, and the only thing they genuinely share is the instant the disruption starts. Drawing the line is not pedantry; it is what makes each half executable.
Two Planes, Not Two Halves of One Document
The DR plane has a named subject. A Postgres cluster, an etcd quorum, a registry, a control plane — something addressable, something an engineer can be paged about. Its units are RTO and RPO. Its plans are indexed by cause: you reach the right procedure by first establishing what broke, because the procedure for a corrupted volume is not the procedure for an unreachable region. Its evidence is emitted by machines that were present — restore logs, timestamps, a recovery point measured after the fact.
The BC plane has a different subject: a mission or business process. Settling a payment, admitting a patient, releasing a shipment. NIST SP 800-34 Rev. 1 puts it plainly — "The BCP focuses on sustaining an organization's mission/business processes during and after a disruption." A process survives the loss of any single system, and it can still be failing after every system is healthy, because the backlog the outage created has not been worked off. Its units are a ceiling the business accepts. Its plans are cause-agnostic: staffing, delegation of authority, customer and supervisor communication, manual workarounds, a defined degraded mode. Nothing in a business continuity plan runs kubectl.
That is the whole boundary, and it is worth stating in one sentence before anything else: disaster recovery is the technical recovery of a system, business continuity is what keeps the business running while that recovery happens. Everything below is the consequence of taking it seriously.
What the Standards Actually Separate
NIST SP 800-34 Rev. 1 is from May 2010 and is not recent guidance, but it remains the cited US federal contingency-planning reference and its definitions are stable — which makes it useful for exactly this argument, because it splits what most operators call "the DR plan" into two documents rather than one.
A Disaster Recovery Plan, in NIST's usage, is "an information system-focused plan designed to restore operability of the target system, application, or computer facility infrastructure at an alternate site after an emergency." The scope clause is the part that surprises people: "The DRP only addresses information system disruptions that require relocation." An in-place restore of a corrupted cluster is therefore not, strictly, a DRP at all.
The document for that is the Information System Contingency Plan. NIST separates them on exactly this basis: "The ISCP differs from a DRP primarily in that the information system contingency plan procedures are developed for recovery of the system regardless of site or location." Site-independence is the distinguishing property, not severity and not blast radius.
Apply that to a typical Kubernetes DR pack and the taxonomy lands cleanly. The cross-region failover — promote the standby cluster, repoint DNS, bring workloads up somewhere else — is a DRP. The per-system restore procedures inside it, the Velero and etcd snapshot mechanics that run identically whether you are recovering in place or in the standby region, are ISCPs. Where one document does both jobs, the relocation decision ends up buried three pages into a procedure that starts by restoring a volume.
The useful move is to split the mixed document along that line rather than relabel it. Relabelling changes a heading. Splitting forces the question the merged document lets you avoid: at what point does this stop being a restore and become a relocation, and who decides? That decision has a cost, a blast radius and usually an approver, and it belongs on its own page.
NIST also defines a Continuity of Operations plan, which "focuses on restoring an organization's mission essential functions (MEF) at an alternate site and performing those functions for up to 30 days before returning to normal operations." That 30-day figure is a US federal construct tied to federal essential functions. It is not a private-sector benchmark and should not be borrowed as one.
MTD, RTO, RPO: Which Number Belongs to Whom
Three numbers, and the common failure is using two names for them. Start with the one most vendor material omits entirely.
The MTD represents the total amount of time the system owner/authorizing official is willing to accept for a mission/business process outage or disruption and includes all impact considerations.
Maximum tolerable downtime is a property of the business process and it is accepted by someone outside engineering. RTO is a different quantity: NIST defines it as "the maximum amount of time that a system resource can remain unavailable before there is an unacceptable impact on other system resources, supported mission/business processes, and the MTD." A system resource, not a business process. That is a number an infrastructure team can measure and propose on its own — still accepted alongside the process owners, leadership and business managers NIST puts in the BIA, and still bounded by the MTD with reprocessing time added.
The relationship between them is arithmetic, not hierarchy. "Because the RTO must ensure that the MTD is not exceeded, the RTO must normally be shorter than the MTD." NIST then gives the reason, and it is the sentence worth committing to memory: "a system outage may prevent a particular process from being completed, and because it takes time to reprocess the data, that additional processing time must be added to the RTO to stay within the time limit established by the MTD."
Read that carefully, because it rules out the diagram everyone draws. The gap between RTO and MTD is not a business-continuity phase that politely begins once the restore finishes. It is work recovery — clearing the backlog the outage created — and it runs while the business continuity plan has already been executing for hours. The two lanes are concurrent. Degraded-mode operation and customer communication start at the disruption, not at the handoff.
RPO sits outside the arithmetic altogether. "Unlike RTO, RPO is not considered as part of MTD. Rather, it is a factor of how much data loss the mission/business process can tolerate during the recovery process." It is a data-loss budget, not a duration in the outage sum — which is why a recovery point has to be measured from a real failover rather than asserted from a plan, and why a declared RPO with no drill behind it is a number about intentions.
The Handoff Is a Declaration Record, Not a Merged Chapter
If the two planes are separate, something has to cross between them. Absent a designed artifact, that something is a person typing "is it back yet?" into a chat channel. Merging the documents is one attempt to fix that. A better one is to make the crossing a single machine-readable artifact with a fixed schema.
A recovery declaration record states what the DR side knows and refuses to state what it does not. It names the system, the restore completion timestamp, the measured recovery point and its provenance, and the degradation still in force. It carries the BC side's inputs in their own fields, kept distinct because they are distinct quantities. And it names what it does not evidence, which is the field that keeps it honest.
# recovery-declaration/2026-09-23-payments-ledger.yaml
# Written by the DR side, read by the BC side. Opened as a stub the moment
# the disruption is detected, with the recovery: block empty, so the MTD
# clock below can run during the outage; the recovery: fields are filled in
# when the restore completes. A file schema kept in git, not a CRD: there is
# no RecoveryDeclaration controller, and kubectl apply would refuse it with
# "no matches for kind".
apiVersion: continuity.example.com/v1
kind: RecoveryDeclaration
metadata:
name: payments-ledger-2026-09-23
namespace: continuity
spec:
system:
name: payments-ledger-postgres
iscp: runbooks/iscp/payments-ledger-postgres.md
relocated: false # false => this was an ISCP execution, not a DRP
disruption:
detected_at: "2026-09-23T02:14:07Z"
declared_at: "2026-09-23T02:19:41Z"
recovery:
restore_complete_at: "2026-09-23T03:02:55Z"
recovery_point: "2026-09-23T02:13:52Z"
recovery_point_source: measured
measurement: pg_wal_lsn_diff against the promoted standby's last received LSN
data_gap_seconds: 15
degradation_in_force:
- description: settlement batch queued, not yet replayed
expected_clear_by: "2026-09-23T05:30:00Z"
- description: reporting replicas serving stale views
expected_clear_by: "2026-09-23T04:00:00Z"
business_process:
name: intraday settlement
mtd: PT4H # NIST MTD, accepted by the process owner
# NIST measures the MTD from the disruption, not from detection. Detection
# is the earliest instant most teams can actually evidence, so it is used
# here as a practical start: the undetected span still counts against the
# MTD, it is simply invisible to this clock, which therefore reads
# optimistic by exactly the detection lag. Where a disruption timestamp can
# be established, set that instead and carry the lag explicitly.
clock_starts_at: "2026-09-23T02:14:07Z"
business_service:
name: customer payment initiation
external_user: true
impact_tolerance: null # PRA SS1/21 quantity; unset outside that perimeter
impact_tolerance_note: >-
Cause-agnostic ceiling on an important business service. Distinct from
the MTD above. Leave null unless the firm is PRA-regulated.
not_evidenced:
- the business process resuming; this record ends at the system boundary
- any statement about cause, which the BC ceilings do not ask about
Once the record exists, the handoff can be a check rather than a conversation. That is also why the record is opened as a stub at detection rather than written at restore completion: the clock's only input is the file, and a file that does not exist until the restore finishes cannot escalate during the outage, which is the window where escalation matters. The continuity clock reads the declaration, compares elapsed process-outage time against the MTD, and exits non-zero when the escalation gate is crossed — so escalation is triggered by arithmetic that nobody has to remember to perform at 04:00. It never contacts a cluster, because nothing it measures lives in one.
#!/usr/bin/env bash
# mtd-clock — read a recovery declaration record and decide whether the
# business process outage has crossed its MTD. Deliberately knows nothing
# about clusters: the MTD is a property of the process, not of the system.
# Exit 0 inside the MTD, 1 once it is reached, 2 on an unusable record.
set -euo pipefail
record="${1:?usage: mtd-clock <recovery-declaration.yaml>}"
process=$(yq '.spec.business_process.name // ""' "$record")
mtd=$(yq '.spec.business_process.mtd // ""' "$record")
started=$(yq '.spec.business_process.clock_starts_at // ""' "$record")
if [ -z "$process" ] || [ -z "$mtd" ] || [ -z "$started" ]; then
printf 'INVALID %s is missing a spec.business_process field\n' "$record" >&2
exit 2
fi
# ISO-8601 duration to seconds, over the PnDTnHnMnS subset. Weeks and months
# are rejected rather than guessed: a month is not a fixed number of seconds.
duration_seconds() {
local spec="$1" date_part time_part token number unit total=0
if ! printf '%s' "$spec" |
grep -Eq '^P([0-9]+D)?(T([0-9]+H)?([0-9]+M)?([0-9]+S)?)?$'; then
printf 'INVALID mtd %s is not a PnDTnHnMnS duration\n' "$spec" >&2
return 2
fi
spec="${spec#P}"
date_part="${spec%%T*}"
time_part=""
case "$spec" in *T*) time_part="${spec#*T}" ;; esac
for token in $(printf '%s' "$date_part" | grep -oE '[0-9]+D' || true); do
number="${token%D}"
total=$((total + number * 86400))
done
for token in $(printf '%s' "$time_part" | grep -oE '[0-9]+[HMS]' || true); do
number="${token%?}"
unit="${token#"$number"}"
case "$unit" in
H) total=$((total + number * 3600)) ;;
M) total=$((total + number * 60)) ;;
S) total=$((total + number)) ;;
esac
done
if [ "$total" -le 0 ]; then
printf 'INVALID mtd %s resolves to zero seconds\n' "$1" >&2
return 2
fi
printf '%s\n' "$total"
}
mtd_seconds=$(duration_seconds "$mtd")
if ! start_epoch=$(date -u -d "$started" +%s 2>/dev/null); then
printf 'INVALID clock_starts_at %s is not a timestamp GNU date accepts\n' \
"$started" >&2
exit 2
fi
elapsed=$(($(date -u +%s) - start_epoch))
if [ "$elapsed" -lt 0 ]; then
printf 'INVALID clock_starts_at %s is in the future; check host clocks\n' \
"$started" >&2
exit 2
fi
restored=$(yq '.spec.recovery.restore_complete_at // ""' "$record")
if [ -n "$restored" ]; then
printf 'note system restore completed at %s; this clock is the other lane\n' \
"$restored"
fi
if [ "$elapsed" -ge "$mtd_seconds" ]; then
printf 'BREACHED %s down %ss against an MTD of %s (%ss). Escalate.\n' \
"$process" "$elapsed" "$mtd" "$mtd_seconds"
exit 1
fi
printf 'WITHIN %s down %ss of %s (%ss); %ss remain.\n' \
"$process" "$elapsed" "$mtd" "$mtd_seconds" "$((mtd_seconds - elapsed))"
Encoding the BIA in the Cluster
A business impact analysis produces a ranked list of processes by criticality. That list is usually a spreadsheet, and the spreadsheet is usually the last place anyone looks when a node is under pressure. The proposal here is simple and, as far as documented industry practice goes, not a standard one: express the BIA's ranking as Kubernetes PriorityClass values, so the business's stated ordering is something the scheduler can act on — and add a check that asserts the two have not diverged.
The value range is documented and worth respecting. "A PriorityClass object can have any 32-bit integer value smaller than or equal to 1 billion," giving a user range of -2147483648 to 1000000000, and "the higher the value, the higher the priority." The built-in critical classes sit above that ceiling — for Kubernetes v1.37 the documentation gives 2000000000 for system-cluster-critical and 2000001000 for system-node-critical — so no BIA tier can be written that outranks cluster-critical infrastructure. That is a property to rely on, not a limitation to work around.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: bia-tier-1-critical
value: 900000000
globalDefault: false
description: >-
BIA tier 1. Example band: processes whose MTD is four hours or less. The
cutoffs across these four classes are illustrative, not drawn from NIST
SP 800-34 or from SS1/21 — set your own from the BIA. Deliberately below
the built-in system-cluster-critical and system-node-critical classes.
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: bia-tier-2-important
value: 600000000
globalDefault: false
description: >-
BIA tier 2. Example band: processes whose MTD is over four hours and up to
twenty-four.
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: bia-tier-3-deferrable
value: 300000000
preemptionPolicy: Never
globalDefault: false
description: >-
BIA tier 3. Example band: MTD over twenty-four hours. Non-preempting:
placed in the
scheduling queue ahead of lower-priority Pods, but cannot preempt them.
Non-preempting Pods may still be preempted by higher-priority Pods.
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: bia-unclassified
value: 0
globalDefault: true
description: >-
Everything the BIA has not ranked. Only one PriorityClass may set
globalDefault; without one, Pods with no priorityClassName take priority
zero regardless. Naming the tier makes the unranked set countable.
preemptionPolicy is deliberately left unset: a globalDefault class hands
its preemptionPolicy to every new Pod that omits priorityClassName, so
setting Never here would quietly change preemption behaviour for the
whole unranked majority, not just record their priority.
The check is the half that makes it worth doing, and it is worth being exact about what it checks. The script below makes three assertions and no more: every class the ranking names exists in the cluster and sits inside the documented user range; the class values still decrease in the ranking's tier order; and each ranked workload's priorityClassName is still the class the BIA assigned it. The first two only prove the tier definitions have not been reshuffled — a cluster with four perfectly ordered classes and not a single workload using them passes both. The third assertion is the one that earns the section: it is what fails when a workload is promoted, demoted, or left unclassified without the process owner hearing about it. It costs the BIA map a third column — <rank> <priorityclass> <namespace>/<kind>/<name> — and that <kind> has to be a controller carrying spec.template.spec.priorityClassName: a Deployment, StatefulSet, DaemonSet or Job. Point it at a bare Pod and the jsonpath comes back empty even when spec.priorityClassName is set on the Pod itself, which reads as drift that is not there.
#!/usr/bin/env bash
# bia-priority-drift — assert that the BIA ranking, the cluster's PriorityClass
# values, and the workloads actually requesting those classes still agree.
# bia-map.txt: "<rank> <priorityclass> <namespace>/<kind>/<name>", one row per
# ranked workload, most critical first, # for comments. <kind> must be a
# controller that carries spec.template.spec.priorityClassName — Deployment,
# StatefulSet, DaemonSet or Job. The jsonpath below reads empty on a bare Pod
# even when spec.priorityClassName is set directly on it.
set -euo pipefail
map="${1:?usage: bia-priority-drift <bia-map.txt> <kube-context>}"
context="${2:?usage: bia-priority-drift <bia-map.txt> <kube-context>}"
classes=$(kubectl --context "$context" get priorityclasses \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.value}{"\n"}{end}')
status=0
previous_value=""
previous_name=""
while read -r rank class workload; do
case "$rank" in '' | '#'*) continue ;; esac
if [ -z "$workload" ]; then
printf 'MALFORMED rank %s: expected "<rank> <class> <ns>/<kind>/<name>"\n' \
"$rank" >&2
status=1
continue
fi
value=$(printf '%s\n' "$classes" | awk -v c="$class" '$1 == c { print $2 }')
if [ -z "$value" ]; then
printf 'MISSING rank %s names %s; no such PriorityClass in the cluster\n' \
"$rank" "$class" >&2
status=1
continue
fi
if [ "$value" -gt 1000000000 ]; then
printf 'RANGE %s is %s, above the 1000000000 ceiling for user classes\n' \
"$class" "$value" >&2
status=1
fi
# Tier ordering. Only compared when the ranking moves to a new class, so a
# tier may hold as many workloads as the BIA puts in it.
if [ "$class" != "$previous_name" ]; then
if [ -n "$previous_value" ] && [ "$value" -ge "$previous_value" ]; then
printf 'DRIFT BIA ranks %s below %s, but cluster values say %s >= %s\n' \
"$class" "$previous_name" "$value" "$previous_value" >&2
status=1
fi
previous_value="$value"
previous_name="$class"
fi
# Tier membership. This is the assertion that a correctly ordered set of
# unused PriorityClasses cannot satisfy.
namespace="${workload%%/*}"
rest="${workload#*/}"
kind="${rest%%/*}"
name="${rest#*/}"
if ! actual=$(kubectl --context "$context" --namespace "$namespace" \
get "$kind" "$name" \
-o jsonpath='{.spec.template.spec.priorityClassName}' 2>/dev/null \
</dev/null); then
printf 'ABSENT %s is ranked %s but does not exist in the cluster\n' \
"$workload" "$rank" >&2
status=1
continue
fi
if [ "$actual" != "$class" ]; then
printf 'DRIFT %s should request %s but requests %s\n' \
"$workload" "$class" "${actual:-<unset, scheduled at the global default>}" >&2
status=1
fi
done <"$map"
if [ "$status" -eq 0 ]; then
echo "BIA ranking, PriorityClass ordering and workload membership agree"
fi
exit "$status"
Four Ways the Boundary Fails
These follow logically from the definitions above rather than from any field survey. Each one is recognisable from the artifacts a team already has.
- No MTD exists, so the RTO is doing two jobs. The symptom is an RTO that was set by the infrastructure team and never accepted by anyone else. NIST's arithmetic says an RTO must be shorter than an MTD with reprocessing time added; with no MTD, the RTO is just what the current architecture happens to achieve, promoted to a target. Diagnostic: ask who signed the number. If the answer is a platform lead, there is no MTD.
- Both planes wait on each other. Engineering does not declare degraded mode because the restore is nearly done; the business does not invoke workarounds because engineering has not declared anything. The merged document actively causes this, because it reads as one sequence with one owner. The declaration record breaks the deadlock by removing the need to agree on a status before either side acts.
- Degraded mode is undefined. The BC plan says the process continues, but nobody has written down what a reduced service level actually is, who may authorise entering it, or what the exit criteria are. A plan that assumes either full service or total outage has no vocabulary for the state the business is actually in for most of an incident.
- The business target is written per cause. Separate ceilings for ransomware, for a region loss, for a supplier failure. A cause-indexed runbook is correct — a restore genuinely differs by failure mode. A cause-indexed business target is not, because the process owner is accepting an outage, and the customer experiencing it does not care which failure mode produced it.
Evidence, Impact Tolerance, and the Audit Angle
That fourth failure has a regulatory analogue worth borrowing from, with the scope stated honestly. The Bank of England's PRA supervisory statement SS1/21 binds UK PRA-regulated firms and nobody else. Within that perimeter, the Operational Resilience Parts "define an impact tolerance as the maximum tolerable level of disruption to an important business service as measured by a length of time in addition to any other relevant metrics."
Two features make it a better-shaped target than an RTO promoted beyond its job. First, it is explicitly cause-agnostic: "The PRA expects impact tolerances to be set on the assumption that a disruption will occur. Firms should not consider the cause or probability of disruption when setting their impact tolerances." Second, it is attached to something the customer can perceive. "Business services deliver a specific outcome or service to an identifiable user external to the firm and should be distinguished from business lines, which are a collection of services and activities." The PRA adds that "firms should not identify internal services alone (for example those provided by human resources or payroll) as important business services" — though such internal services, where necessary to deliver an important business service, are still included in the mapping and scenario testing.
The observation the standards do not make, and which is this article's own argument rather than the PRA's: a cause-agnostic target cannot be discharged by a cause-indexed runbook set alone. The runbook answers "how long, given this failure mode." The target does not ask about failure modes. Satisfying it means being able to say that every plausible path to that service being unavailable lands inside the ceiling — which is a mapping exercise across the runbooks, not a property of any one of them.
Evidence follows the same split, and this is where the merged document costs real audit time. DR-plane evidence is machine-emitted: restore logs, drill records, a measured recovery point. BC-plane evidence is human-generated: declaration records, communication logs, the decision to enter and leave degraded mode and who took it. An assessor asking how a self-hosted stack evidences operational resilience is asking for both, and a merged document forces someone to disentangle them under time pressure. Keeping them apart means each side can be produced by the team that owns it. It also means proving a restore actually works stays a DR-plane obligation with its own artifacts, rather than a claim buried in a continuity narrative.
Exit Ramps and the Long Game
The commercial pressure runs the other way. Combined BCDR suites sell one platform that owns the plan documents, the call trees, the declaration workflow and often the replication itself — and a plan you cannot read without a licence is a plan you have partly outsourced. The same logic applies here as in any DRaaS versus owned-cluster decision: the recovery capability can be bought, but the definitions should not be.
Keeping the boundary portable costs little. The declaration record is YAML in a git repository. The MTD and the ranked BIA list are plain text. The drift check is a short shell script against kubectl. None of it is coupled to a vendor's data model, and all of it survives a migration of the tooling underneath. A platform that renders a nicer call tree is worth paying for; one that becomes the only place your MTDs exist is not.
The long-game argument is about what triggers a review. Infrastructure changes constantly, and the DR plane has to keep up — new systems, new failure modes, new runbooks. The BC plane should be far more stable, because business processes change on a slower clock than clusters do. When the two live in one document, every infrastructure change reopens the business ceilings, and nobody re-accepts them properly, so they quietly become whatever engineering last wrote. Separate them and the MTD gets revisited when the business changes — a new market, a new regulatory perimeter, a new customer commitment — which is the only thing that should ever move it.
§FAQ/Common questions
Frequently asked
What is the difference between a disaster recovery plan and a business continuity plan?
A disaster recovery plan restores a named technical system and is measured in RTO and RPO. A business continuity plan sustains the mission or business process that system serves, and is bounded by a maximum tolerable downtime accepted outside engineering. NIST SP 800-34 Rev. 1 describes the BCP as focused on sustaining an organisation's mission/business processes during and after a disruption; the recovery of the system itself sits in a separate plan.
Is RTO the same as maximum tolerable downtime?
No, and conflating them is the most common error in this area. NIST SP 800-34 Rev. 1 defines MTD as the total outage time the system owner or authorizing official will accept for a mission/business process, while RTO is the maximum time a system resource can be unavailable. RTO must normally be shorter than MTD, because the time taken to reprocess data after the outage has to be added to the RTO to stay inside the MTD.
Should disaster recovery and business continuity be one document or two?
Two. They have different owners, different units, different audiences and different evidence. A merged document inherits the DR side's cause index and the BC side's audience, and serves neither well. The single artifact that needs to cross between them is a recovery declaration record stating what was restored, when, to what measured data point, and what degradation remains in force.
Is an in-place Kubernetes cluster restore a disaster recovery plan?
Not in NIST's taxonomy. NIST SP 800-34 Rev. 1 states that the DRP only addresses information system disruptions that require relocation, and that an Information System Contingency Plan differs from a DRP in that its procedures are developed for recovery of the system regardless of site or location. A cross-region Kubernetes failover pack is a DRP; the per-system restore procedures inside it are ISCPs.
Does impact tolerance apply outside UK regulated firms?
As an obligation, no — the PRA's SS1/21 binds UK PRA-regulated firms only. As a design idea it travels well, because it is set on the assumption a disruption will occur and without regard to its cause or probability, and it attaches to a service delivering an outcome to a user external to the firm. That shape makes a better business ceiling than an RTO stretched past what it measures.
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.