Computing Business-Day FOIA Deadlines with Federal Holiday Calendars
Within Deadline Tracking & Escalation, the single computation that everything else depends on is the deadline date itself — and for federal FOIA that date is a business-day figure, not a calendar-day one. The 20-business-day response window under 5 U.S.C. § 552(a)(6)(A)(i) excludes Saturdays, Sundays, and legal public holidays, so a naive received_date + timedelta(days=20) produces a date that is systematically too early and, worse, wrong by a variable amount that depends on which weekends and holidays fall inside the window. This page shows how to compute the deadline correctly: a maintained federal holiday calendar, weekend roll-over, the unusual-circumstances extension, and an audit line that makes the arithmetic reproducible.
Why Calendar Math Understates the Deadline
Consider a request received on Friday, November 20, 2026. Counting twenty calendar days lands on December 10. Counting twenty business days — skipping four weekends, Veterans Day-adjacent observances, and the Thanksgiving holiday on November 26 — lands the true deadline in mid-to-late December. An agency that logs the calendar-day figure believes it has less time than the statute allows and may rush or mis-prioritize; an agency that uses calendar math in the other direction, applying it to a business-day statute while assuming a comfortable margin, can breach outright. Either way, the recorded deadline diverges from the legal one, and the divergence is not constant — it grows with every weekend and holiday inside the window, which is exactly why it cannot be approximated with a fixed fudge factor.
The correct approach counts forward one calendar day at a time, decrementing the remaining-business-days counter only on days that are neither a weekend nor a holiday. The receipt day itself does not consume a count; the clock advances from the next day. Getting that boundary right, and getting the holiday set right, is the whole job. This math is the foundation the escalation ladder in Automated Escalation Alerts Before a FOIA Deadline Breach reads its days-remaining figure from, so an error here propagates into every tier that fires late or early.
Building the Holiday Calendar
The federal holiday set is the input the whole computation turns on, and it is not static: some holidays fall on fixed dates (Juneteenth on June 19, Independence Day on July 4), others float (Thanksgiving on the fourth Thursday of November), and fixed-date holidays that land on a weekend are observed on an adjacent weekday — a Saturday holiday is observed the preceding Friday, a Sunday holiday the following Monday. A production calendar encodes those observance rules and covers every year a request might span. For clarity the example below uses an explicit set for the relevant year; a real deployment generates the set from the observance rules or loads a maintained table, and reloads it as new years come into range.
The correct set is what separates a deadline that survives audit from one that is quietly wrong. A missing holiday shortens the computed window by a day and can turn a compliant response into a recorded breach; a spurious one lengthens it and can lull an analyst past the true deadline. Treat the calendar as compliance-critical configuration, version it, and test it against known reference dates.
The Deadline Computation in Python
The implementation below counts business days from receipt, rolls over weekends and holidays, applies the unusual-circumstances extension under § 552(a)(6)(B), and emits a structured JSON audit line recording the receipt date, the window, and the resulting deadline. It is standard-library only and deterministic: the same receipt date and holiday set always yield the same deadline.
"""
business_day_deadline.py
Compute a FOIA business-day deadline with a federal holiday calendar.
5 U.S.C. § 552(a)(6)(A)(i): a determination is due within 20 business days
of receipt; § 552(a)(6)(B) allows a 10-working-day unusual-circumstances
extension. Weekends and legal public holidays are excluded from the count.
"""
import json
import logging
import sys
from datetime import date, timedelta
# ---------------------------------------------------------------------------
# Structured audit logging so every computed deadline is reproducible (AU-2).
# ---------------------------------------------------------------------------
class JSONFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
entry = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"message": record.getMessage(),
"request_id": getattr(record, "request_id", None),
"received_on": getattr(record, "received_on", None),
"deadline": getattr(record, "deadline", None),
"business_days": getattr(record, "business_days", None),
}
return json.dumps(entry)
audit = logging.getLogger("foia_deadline_math")
audit.setLevel(logging.INFO)
_handler = logging.StreamHandler(sys.stdout)
_handler.setFormatter(JSONFormatter())
audit.addHandler(_handler)
def _nearest_weekday(day: date) -> date:
# Observance rule: a Saturday holiday is observed the preceding Friday,
# a Sunday holiday the following Monday.
if day.weekday() == 5:
return day - timedelta(days=1)
if day.weekday() == 6:
return day + timedelta(days=1)
return day
def federal_holidays(year: int) -> frozenset:
"""Observed federal legal public holidays for a single year."""
fixed = [
date(year, 1, 1), # New Year's Day
date(year, 6, 19), # Juneteenth
date(year, 7, 4), # Independence Day
date(year, 11, 11), # Veterans Day
date(year, 12, 25), # Christmas Day
]
def nth_weekday(month: int, weekday: int, n: int) -> date:
d = date(year, month, 1)
d += timedelta(days=(weekday - d.weekday()) % 7)
return d + timedelta(weeks=n - 1)
def last_weekday(month: int, weekday: int) -> date:
d = nth_weekday(month, weekday, 4)
nxt = d + timedelta(weeks=1)
return nxt if nxt.month == month else d
floating = [
nth_weekday(1, 0, 3), # MLK Jr. Day — 3rd Monday of January
nth_weekday(2, 0, 3), # Washington's Birthday — 3rd Monday of February
last_weekday(5, 0), # Memorial Day — last Monday of May
nth_weekday(9, 0, 1), # Labor Day — 1st Monday of September
nth_weekday(10, 0, 2), # Columbus Day — 2nd Monday of October
nth_weekday(11, 3, 4), # Thanksgiving — 4th Thursday of November
]
return frozenset(_nearest_weekday(d) for d in fixed) | frozenset(floating)
def is_business_day(day: date) -> bool:
# Monday=0 .. Sunday=6; weekends and observed holidays never count.
return day.weekday() < 5 and day not in federal_holidays(day.year)
def add_business_days(start: date, count: int) -> date:
"""Advance `count` business days from receipt; the receipt day is day zero."""
cursor, remaining = start, count
while remaining > 0:
cursor += timedelta(days=1) # roll to the next calendar day first
if is_business_day(cursor): # only business days decrement the count
remaining -= 1
return cursor
def foia_deadline(request_id: str, received_on: date,
unusual_circumstances: bool = False) -> date:
"""Compute the FOIA response deadline, logging the math for audit."""
try:
window = 20 # 5 U.S.C. § 552(a)(6)(A)(i)
deadline = add_business_days(received_on, window)
if unusual_circumstances:
# § 552(a)(6)(B): up to 10 additional working days.
deadline = add_business_days(deadline, 10)
window += 10
audit.info(
f"deadline computed for {request_id}",
extra={"request_id": request_id, "received_on": received_on.isoformat(),
"deadline": deadline.isoformat(), "business_days": window},
)
return deadline
except (TypeError, ValueError) as exc:
# A malformed receipt date must fail loudly, never default silently.
audit.error(
f"deadline computation failed for {request_id}: {exc}",
extra={"request_id": request_id},
)
raise
if __name__ == "__main__":
base = foia_deadline("REQ-2026-0781", date(2026, 11, 20))
extended = foia_deadline("REQ-2026-0781", date(2026, 11, 20),
unusual_circumstances=True)
print(json.dumps({"base_deadline": base.isoformat(),
"extended_deadline": extended.isoformat()}, indent=2))
Running the module prints the base and extended deadlines and emits an audit line for each. Because the holiday set is derived from observance rules and the count advances one calendar day at a time, the Thanksgiving week in the example is handled correctly without any special-casing at the call site.
Verifying the Math
Treat the deadline function as testable, not trusted. Three checks catch the failure modes that matter:
- Known reference dates. Assert the deadline for a receipt with no intervening holidays lands exactly four weeks and a day out (twenty business days over four clean weeks), and assert a receipt straddling Thanksgiving lands one business day later than the same span without the holiday.
- Observance roll-over. Assert that Independence Day on a Saturday is observed the preceding Friday and that the Friday, not the Saturday, is the day excluded from the count.
- Boundary correctness. Assert the receipt day itself never consumes a business day and that a receipt on a Friday advances into the following week rather than counting the weekend.
For requests under a state statute rather than federal FOIA, the counting basis and holiday rules can differ — some states count calendar days and observe state-specific holidays. The worked contrast in Calculating California Public Records Act Deadlines in Python shows how a calendar-day statute changes the arithmetic, and the per-jurisdiction windows are catalogued in State Law Compliance Frameworks. Whichever basis applies, the computed deadline and the calendar version used to compute it should be written to the shared store described in Audit Logging Architecture so the figure is reproducible on review.
Compliance Verification Checklist
FAQ
Does the receipt day count as the first business day of the window?
No. The receipt day is day zero; the twenty-business-day count advances from the next calendar day. Counting the receipt day itself would shorten the window by one business day and understate the agency’s true obligation. The add_business_days function reflects this by rolling to the next day before testing whether to decrement the counter.
How are federal holidays that fall on a weekend handled?
They are observed on the nearest weekday and excluded on that observed date. A holiday on a Saturday is observed the preceding Friday; one on a Sunday is observed the following Monday. The Saturday or Sunday is already excluded as a weekend, so the effect is that the adjacent weekday is also removed from the count. Encoding the observance rule prevents the off-by-one error of excluding the nominal date instead of the observed one.
What changes for a state request that counts calendar days?
The counting basis flips from business days to a plain calendar-day addition, and the applicable holiday set may differ or be irrelevant. Rather than branch inside the FOIA function, resolve the jurisdiction’s basis and window first and dispatch to the matching computation, as catalogued in the state-law reference. A calendar-day statute like the California Public Records Act does not skip weekends in the same way, so reusing the business-day function would compute the wrong date.
Related
- Deadline Tracking & Escalation — the parent system this deadline math feeds its days-remaining figure into.
- Automated Escalation Alerts Before a FOIA Deadline Breach — the tiered alerts that fire off the days-remaining this computation produces.
- State Law Compliance Frameworks — per-jurisdiction windows and counting bases for non-federal requests.
- Calculating California Public Records Act Deadlines in Python — how a calendar-day statute changes the arithmetic.
- Audit Logging Architecture — the store where each computed deadline and its calendar version are recorded.