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.

Business-day counting flow with weekend and holiday roll-over From the receipt date with days_left set to twenty, the algorithm advances one calendar day. A decision tests whether that day is a weekend or a federal holiday; if so, the day is skipped and the loop advances again without decrementing. If the day is a business day, days_left decreases by one. A second decision tests whether days_left has reached zero; if not, the loop continues; if so, the statutory deadline has been reached. Receipt date · days_left = 20 Advance one calendar day Weekend or holiday? Skip — not counted no decrement days_left -= 1 days_left == 0? Statutory deadline reached yes no yes no

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.

python
"""
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.