Rest Period Compliance Checks
Rest period validation answers a deceptively narrow question — did this crew member get enough time off between duties? — that turns out to hide most of the temporal complexity in a scheduling system. A rest period is not a single number to compare against a minimum; it is a window bounded by the end of one duty and the start of the next, measured in the local time of the place the crew rests, discounted for interruptions, and simultaneously subject to a rolling weekly requirement that no single-window check can see. Within the Duty Time Validation & Rule Engines domain, rest validation is the constraint layer that a pairing engine hits most often and trusts most completely, because a mis-computed rest window silently propagates into every downstream flight duty period the engine then believes is legal. This page covers how rest windows are modelled, which regulatory provisions drive the schema, how the checks are implemented in Python, and how they connect to the rest of the validation pipeline.
The Rest Validation Problem Behind a Simple Minimum
The scoped engineering challenge is precise: given a proposed sequence of duties for a crew member, confirm that every gap between consecutive duties satisfies the applicable minimum rest, that any rest taken inside a duty under split-duty provisions is credited correctly, and that the crew member has received a qualifying long rest somewhere in the preceding rolling window. Three properties make this genuinely hard, and none of them are captured by a naive subtraction of two timestamps.
First, rest is measured in the local time of the rest facility, not in UTC and not in the crew member’s home base time. A ten-hour rest that begins at 23:00 local and ends at 09:00 local is a very different recovery opportunity from one that runs 11:00 to 21:00, and the regulations that govern its duration are unforgiving about which clock is authoritative. Because the rest window is bounded by segment end and segment start times that arrive from operational feeds in UTC, the validator must convert to the facility zone, apply the duration rule there, and convert back — and it must do so across daylight-saving transitions where a nominal ten-hour local window is nine or eleven real hours.
Second, rest is not always a clean gap between duties. Split-duty provisions let an operator interrupt a flight duty period with a rest opportunity in a suitable accommodation, crediting some of that break back against the duty limit. A validator that only inspects the gaps between duties will miss the rest taken within one, and will either reject a legal split-duty pairing or, worse, fail to extend the flight duty period the split rest legitimately unlocks.
Third, rest compliance is simultaneously an instantaneous check and a rolling-window check. The minimum-rest-before-duty rule is local to a single gap, but the weekly-rest rule — a required block of consecutive hours free from all duty within a rolling 168-hour window — can only be evaluated by scanning history. A pairing that satisfies every individual gap can still be illegal because no single gap in the preceding week was long enough to qualify as the weekly rest.
The consequence is that rest validation is not one comparison but a small family of related checks that share a normalized event stream and must agree with each other. Building them as independent conditionals is how engines end up passing a roster on the gap check while silently violating the weekly-rest requirement.
Schema and Data Structure Design
The data model separates three concerns that flat implementations collapse into a single duty table: the raw duty and segment history that bounds each rest window, the derived rest_period rows the validator actually reasons about, and the effective-dated rule parameters that supply the minimum thresholds. Keeping the thresholds in their own effective-dated table is what lets a contractual buffer or a regulatory revision be diffed and rolled back without touching validation code.
The core entities are the crew_member; the duty_period rows with their bounding report and release instants; the flight_segment rows that compose each duty; the derived rest_period that spans the gap between one duty’s release and the next duty’s report, carrying the facility IANA zone, the raw and restorative durations, and an interruption flag; the split_rest rows that record a qualifying in-duty break and the accommodation class that makes it eligible; and the effective-dated rest_rule supplying the minimum-rest value, the weekly-rest block length, the rolling-window length, and the jurisdiction key. Every temporal column is stored in UTC with the originating IANA zone retained as metadata, because the duration rule is applied in facility-local time while the rolling weekly window is summed in UTC.
rest_period the validator reasons about is derived — its raw and restorative hours are computed from consecutive duties, never entered by hand, and governed by an effective-dated rest_rule.Field names, the boundary between what counts as duty versus rest, and the classification of positioning, standby, and reserve time all follow the shared crew duty time taxonomy. Anchoring the schema to that taxonomy is what stops the validator from mistaking a positioning segment for rest, or a callable standby for genuine time free from duty — the two most common ways a rest check silently over-credits recovery. The raw duration of each window in turn depends on precise segment boundaries produced by the Flight Time Calculation Algorithms, because an unaccounted taxi buffer or a mis-partitioned ground segment shifts the release instant that bounds the rest window.
The end-to-end flow from raw logs to a compliance verdict is summarised below: a multi-source duty stream is normalized to UTC, partitioned into airborne, ground, and rest components, and each rest window is checked against the applicable minimum, with marginal-but-legal windows escalated to a fatigue review rather than passed silently.
Regulatory Mapping
Rest validation encodes specific regulatory provisions, and the schema decisions above trace directly to their text. The authoritative US wording lives in FAA Part 117, and the European counterpart under EASA CS-FTL derives from Regulation (EU) No 965/2012, Annex III, Subpart FTL. The mapping below is the ground truth the checks implement.
- §117.25(e) — Minimum rest before a flight duty period. Requires a minimum of 10 consecutive hours of rest before beginning a flight duty period, and that rest period must provide the crew member with a minimum of 8 hours of uninterrupted sleep opportunity. This is why the
rest_periodentity carries both a raw duration and an interruption flag — the 10-hour envelope and the 8-hour uninterrupted sleep opportunity are distinct conditions, and a window that satisfies the first can still fail the second. - §117.25(b) — Weekly rest. Requires that no crew member accept, and no certificate holder assign, a reserve or flight duty period unless the crew member has had at least 30 consecutive hours free from all duty within the preceding 168 consecutive hours. This provision is what forces the rolling-window logic; it cannot be evaluated on a single gap, and it drives the
window_hoursandweekly_rest_hoursparameters onrest_rule. - §117.15 — Split duty. Permits a rest opportunity taken in suitable accommodation during a flight duty period to be excluded from that flight duty period when the qualifying conditions are met, extending the maximum permissible duty. This is the regulatory basis for the
split_restentity and the reason the validator must inspect rest taken inside a duty, not only the gaps between duties. - §117.3 — Definitions. Fixes the meaning of rest period, suitable accommodation, and rest facility, which the schema encodes as the
accommodation_classon split rest and the eligibility test that governs whether a break counts at all. - EASA ORO.FTL.235 — Rest periods. Requires a minimum rest at home base at least as long as the preceding duty period or 12 hours, whichever is greater, and away from home base at least as long as the preceding duty period or 10 hours, whichever is greater. This duration-relative-to-preceding-duty rule is why
rest_ruleis parameterised rather than a fixed constant — the European minimum is a function of the prior duty, not a flat floor. - EASA ORO.FTL.205 with CS FTL.1.235 — Flight duty period and reduced rest. Supplies the interpretations for reduced rest and the accommodation criteria that govern when a shortened rest is permissible, which the validator applies as a jurisdiction-keyed override on the base minimum.
The distinction between when these provisions merely warn and when they hard-block a pairing is worked through against the underlying tables in FAA Part 117 rule schema design, and the European branch against EASA FTL compliance. The rest checks here consume those ceilings; they do not redefine them.
Python Implementation Walkthrough
Production implementations express the rest inputs as typed models so a malformed payload fails at the boundary rather than deep inside the temporal core. Using pydantic for the contract guarantees that duty boundaries are timezone-aware and that the facility zone is present before any duration is computed. All arithmetic runs on aware instants; the facility zone is applied only to select the correct local wall-clock window, never to do naive subtraction.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from pydantic import BaseModel, field_validator
class DutyPeriod(BaseModel):
duty_id: str
report_utc: datetime
release_utc: datetime
facility_zone: str # IANA zone of the rest facility after release
@field_validator("report_utc", "release_utc")
@classmethod
def must_be_aware(cls, value: datetime) -> datetime:
if value.tzinfo is None:
raise ValueError("duty boundaries must be timezone-aware (UTC)")
return value
class RestRule(BaseModel):
min_rest_hours: float # e.g. 10.0 under FAA §117.25(e)
weekly_rest_hours: float # e.g. 30.0 under FAA §117.25(b)
window_hours: float # e.g. 168.0 rolling window
buffer_minutes: int = 0 # contractual buffer above the floor
The single-gap check is the simplest, but its correctness depends entirely on computing the duration from aware instants. Subtracting two aware datetimes yields the true elapsed time regardless of any daylight-saving transition the local clock crossed, which is exactly the behaviour the regulation intends.
def rest_gap_hours(prev: DutyPeriod, nxt: DutyPeriod) -> float:
"""True elapsed rest between one duty's release and the next report."""
delta = nxt.report_utc - prev.release_utc
return delta.total_seconds() / 3600.0
def check_min_rest(prev: DutyPeriod, nxt: DutyPeriod, rule: RestRule) -> dict:
required = rule.min_rest_hours + rule.buffer_minutes / 60.0
actual = rest_gap_hours(prev, nxt)
shortfall = round(required - actual, 4)
return {
"duty_id": nxt.duty_id,
"required_hours": required,
"actual_hours": round(actual, 4),
"compliant": actual >= required,
"shortfall_hours": max(shortfall, 0.0),
}
The EASA duration-relative-to-preceding-duty rule is a thin variation on the same function: the required minimum becomes the greater of the fixed floor and the preceding duty’s length, selected by jurisdiction. Expressing the minimum as a function of the prior duty rather than a constant keeps the home-base 12-hour and away-from-base 10-hour cases in one code path.
def easa_min_rest_hours(prev: DutyPeriod, floor_hours: float) -> float:
duty_len = (prev.release_utc - prev.report_utc).total_seconds() / 3600.0
return max(floor_hours, duty_len)
The 8-hour uninterrupted sleep opportunity under §117.25(e) is a second, distinct condition. Because the sleep opportunity must fall inside the rest window and be uninterrupted, the validator subtracts any recorded interruption from the window and confirms the largest remaining uninterrupted block meets the sleep floor — a rest window can satisfy the 10-hour envelope yet fail the 8-hour opportunity if it was broken by a callout.
Rolling Windows and Temporal Aggregation
The weekly-rest requirement is a rolling-window problem that is far cleaner to express in SQL than to hand-roll in Python. For every duty a crew member is assigned, the engine must confirm that at least one qualifying long rest exists within the trailing 168-hour window. A window function keyed on the crew member, ordered by time, with a RANGE frame expressed as an interval, evaluates every duty against its own trailing week in a single pass.
SELECT
d.crew_id,
d.duty_id,
d.report_utc,
MAX(r.rest_hours) OVER (
PARTITION BY d.crew_id
ORDER BY d.report_utc
RANGE BETWEEN INTERVAL '168 hours' PRECEDING AND CURRENT ROW
) AS longest_rest_in_window
FROM duty_period AS d
JOIN rest_period AS r
ON r.crew_id = d.crew_id
AND r.ends_utc <= d.report_utc
GROUP BY d.crew_id, d.duty_id, d.report_utc;
Duties whose longest_rest_in_window falls below the weekly_rest_hours threshold are the weekly-rest violations. The same rolling pattern generalises to cumulative-rest reporting: swapping MAX for SUM over the frame yields total off-duty hours in the trailing window, which the fatigue layer consumes as a sleep-debt proxy. When the evaluation runs inside the pairing optimiser rather than a reporting job, the equivalent Polars expression keeps the computation in-process:
import polars as pl
weekly = (
duties.sort("report_utc")
.rolling(index_column="report_utc", period="168h", group_by="crew_id")
.agg(pl.col("rest_hours").max().alias("longest_rest_in_window"))
)
The critical correctness detail is that both the SQL RANGE frame and the Polars rolling window operate on the UTC instant, never on local wall-clock time. A date-line crossing makes local ordering diverge from UTC ordering, and summing a trailing window on local time would double-count or skip a day precisely on the long-haul rotations the weekly-rest rule exists to protect.
Integration Points
Rest validation does not stand alone; it is a node in the wider pipeline and only as correct as the data feeding it. Upstream, it depends on flight data ingestion to deliver normalized, deduplicated duty and segment events with authoritative timestamps — a missing release event or a duplicated segment corrupts the window boundaries before any rule runs. The raw window durations are only trustworthy once the block-to-block reductions from the Flight Time Calculation Algorithms have partitioned airborne and ground time correctly.
Downstream, the restorative fraction of each rest window is exactly the input the Fatigue Risk Scoring Models weight sleep debt against, so a rest that is legal but scheduled entirely across daytime should pass the hard check while still lowering the fatigue score. Where a window clears the regulatory floor but sits close to it, the verdict is routed through Threshold Tuning & Alerting, which decides whether a near-minimum rest becomes an informational note, a warning, or an escalation to a scheduler. Every verdict — pass or fail, with its computed shortfall and the rule version applied — is written to the audit log so a compliance team can reconstruct why the engine believed a rest was sufficient six months after the fact.
Testing and Edge Cases
Rest checks fail in production on the boundaries, so the test suite is built around them rather than the happy path. Property-based tests assert invariants that must hold for any input: adding rest to a window never turns a compliant gap non-compliant, and the check is deterministic for identical inputs and rule versions.
- Daylight-saving transitions. A ten-hour local rest window that spans a spring-forward transition is nine real clock hours; a window spanning a fall-back transition is eleven. Tests must pin that the duration is computed from aware instants, so the true elapsed time — not the naive local subtraction — decides compliance.
- Split-rest crediting. A rest opportunity taken inside a duty under §117.15 must be credited against the duty limit only when the accommodation class qualifies; tests must cover both a qualifying suitable accommodation and a non-qualifying break of identical length, which must produce opposite verdicts.
- Weekly-rest boundary. A qualifying long rest that ends exactly 168 hours before a report sits on the edge of the rolling window; the frame is inclusive of the boundary, and tests must pin behaviour on both sides so a rest is neither double-counted nor dropped.
- Date-line crossing. An eastbound rotation that crosses the International Date Line makes UTC ordering and local ordering diverge; the rolling window must be computed on UTC-ordered events so the trailing week is never miscounted.
- Interrupted sleep opportunity. A ten-hour rest window broken by a two-hour callout satisfies the §117.25(e) envelope but fails the 8-hour uninterrupted sleep opportunity; the two conditions must be tested independently, not collapsed into a single duration comparison.
Rule thresholds are version-controlled and validated against the operator’s published FTL scheme before every deployment, so a contractual buffer change or a regulatory revision is a reviewable diff rather than a silent behaviour change — the same discipline the fatigue and flight-time layers follow.
Explore This Topic in Depth
The general checks above become concrete in the step-by-step implementation guides beneath this topic:
- Automating 10-Hour Rest Period Validation — a full walkthrough that parses crew duty logs, computes rest windows with timezone-aware arithmetic, applies a configurable threshold above the §117.25(e) floor, and emits deterministic violation records with an audit trail.
Related
- Flight Time Calculation Algorithms — the upstream block-time partitioning that fixes the release instant bounding each rest window.
- Fatigue Risk Scoring Models — consumes the restorative fraction of each rest window as a sleep-debt input.
- Threshold Tuning & Alerting — routes near-minimum rest verdicts into informational, warning, and escalation tiers.
- FAA Part 117 rule schema design — the hard rest and FDP tables the checks consume.
- EASA FTL compliance frameworks — the European ORO.FTL.235 rest rules for dual-jurisdiction carriers.
Back to Duty Time Validation & Rule Engines.