How to test Same Day ACH cutoff times and settlement timing across time zones
To test Same Day ACH (Automated Clearing House) cutoff times, convert timestamps to Eastern Time (ET), assign the FedACH banking day, and assert settlement. A credit of $1,000,000 or less, not an International ACH Transaction or Automated Enrollment Entry, with a current, blank, or past effective date, received 10:29 a.m. ET on 15 March 2027 settles at 1:00 p.m. ET.
Where a missed cutoff costs money
A scheduler stores the Same Day ACH deadline as 15:30 UTC because that equals 10:30 a.m. Eastern Time in winter. On Monday 15 March 2027, the day after the US clock change, 15:30 UTC is 11:30 a.m. EDT, so a scheduler that releases at its stored deadline misses the first window and the files settle at 5:00 p.m. instead of 1:00 p.m. For a payroll product, wages settle four hours later than the product promised employees; for a business account, a file that also misses the 4:45 p.m. window moves the value date the ledger uses for balances and interest to the next banking day.
The opposite defect costs a fee. A product that fills the Effective Entry Date with yesterday's date to force "next day" processing gets same-day settlement instead. The Federal Reserve Same Day ACH FAQ (checked on 2026-09-22) states that a batch "will be eligible for same day settlement" when the effective entry date is "invalid or stale", provided it arrives by 4:45 p.m. ET, is not IAT or ENR, and each entry is $1,000,000 or less.
The schedule the test encodes
The FedACH Processing Schedule (effective 12 September 2022, checked on 2026-09-22) sets these same-day windows for forward items. All times are Eastern Time (ET):
| Transmission deadline (ET) | Target distribution (ET) | Settlement (ET) |
|---|---|---|
| 10:30 a.m. | Noon | 1:00 p.m. current day |
| 2:45 p.m. | 4:00 p.m. | 5:00 p.m. current day |
| 4:45 p.m. | 5:30 p.m. | 6:00 p.m. current day |
Future-dated items have further deadlines at 8:00 p.m. (Sunday through Thursday only), 10:45 p.m., and 2:15 a.m., all settling at 8:30 a.m. ET on a future business day. Footnote 1 requires files to be "completely received" by the deadline, so a transmission that starts at 10:29 and ends at 10:31 misses the first window.
Operating Circular 4 (effective 5 January 2026), Appendix B, adds the rules a date calculator needs:
- Banking day. Section 1.1 defines the FedACH banking day for receipt as 3:00 a.m. ET to 2:59 a.m. ET on the next calendar day, and notes that other times apply around weekends and holidays.
- Effective date windows. Section 2.1 says items "should specify" an effective date one or two banking days ahead for credits and one banking day for debits; an item dated later than its window is returned to the sender.
- Same-day qualification. Paragraph 3.3(b) of the circular states that a same day item "must specify in the effective entry date field either no value or any value other than a date that is later than the banking day the Reserve Bank receives the item", may use any Standard Entry Class code except IAT or ENR, and must not exceed the Nacha dollar limit. Section 3.2 settles a same-day item without an effective date on the banking day of receipt.
- Settlement dates. Under section 3.1, a same-day item received after the last same-day cutoff settles on the next banking day. Section 3.2 moves a settlement date that is not a banking day to the next banking day.
The same FAQ states that stale-dated forward entries submitted after 4:45 p.m. and before 2:15 a.m. ET do not settle same day. The dollar limit and its scheduled change are on our Nacha testing requirements page.
Two other rails matter when a product routes by deadline:
| Rail | Cut-off or hours | Time basis | Source |
|---|---|---|---|
| Fedwire Funds Service | Customer transfers 6:45 p.m.; business day opens 9:00 p.m. the previous calendar day | ET | Federal Reserve operating hours, checked on 2026-09-22 |
| T2 RTGS, euro | Customer payments 17:00; interbank payments 18:00 (end of RTGS RTS II) | Local time at the ECB seat, CET or CEST | Information Guide for TARGET participants, Part 2, version April 2025 |
| TIPS (SEPA Instant settlement) | No cut-off; settles 24/7/365 | Business day changes after CLM and RTGS closure | ECB TIPS page and TARGET operating schedule, checked on 2026-09-22 |
T2 is closed on weekends, New Year's Day, Good Friday, Easter Monday, 1 May, Christmas Day, and 26 December, per the ECB T2 page. On 9 October 2025 the Federal Reserve announced Sunday and weekday-holiday operation for Fedwire Funds in 2028 or 2029, with voluntary participation, so keep operating days in configuration.
Four dates on one ACH entry
A product that copies the Effective Entry Date into its ledger value date posts a stale-dated credit to a day before it settled. An ACH credit carries four dates:
- Effective Entry Date, positions 70-75 of the batch header, YYMMDD: "The banking day the Originator intends a batch of entries to be settled", per the Nacha ACH developer guide.
- Settlement Date (Julian), positions 76-78: "Inserted by ACH Operator." The originator leaves it blank.
- Posting date: the day the product books the entry in its own ledger.
- Value date: the day the funds count for balance and interest. For ACH it equals the settlement date, which can differ from the effective date.
The same batch header as sent by the originator and as delivered by the ACH Operator, with synthetic identifiers:
5220SYNTHETIC PAYRL 1234567890PPDPAYROLL MAR 12270312 1123456780000001
5220SYNTHETIC PAYRL 1234567890PPDPAYROLL MAR 122703120711123456780000001The first line leaves positions 76-78 blank. The second carries 071, day 71 of 2027, which is 12 March. Keep real account data out of these fixtures. When a bank reports the credit in a camt.053 statement, the entry carries <BookgDt> and <ValDt> separately; our camt.053 reconciliation guide shows how to match them.
Procedure
1. Build the calendar from the Federal Reserve schedule
Take closures from the Federal Reserve holiday schedule (checked on 2026-09-22). Its notes state that Reserve Banks open the preceding Friday when a holiday falls on Saturday and close the following Monday when it falls on Sunday. In 2027, Juneteenth (19 June) and Christmas Day (25 December) fall on Saturday, so Friday 18 June and Friday 24 December are banking days. Independence Day falls on Sunday, so Monday 5 July 2027 is closed.
2. Write the matrix as data
settlement_matrix.csv, one row per boundary. Timestamps are when the file is completely received, in UTC. Amounts are in USD.
case_id,submitted_utc,effective_entry_date,sec_code,direction,amount_usd,expected
S01,2027-03-12T15:29:00Z,270312,PPD,credit,2500.00,2027-03-12 13:00
S02,2027-03-12T15:31:00Z,270312,PPD,credit,2500.00,2027-03-12 17:00
S03,2027-03-15T14:29:00Z,270315,PPD,credit,2500.00,2027-03-15 13:00
S04,2027-03-15T15:29:00Z,270315,PPD,credit,2500.00,2027-03-15 17:00
S05,2027-03-15T20:44:00Z,270315,CCD,credit,2500.00,2027-03-15 18:00
S06,2027-03-15T20:46:00Z,270315,CCD,credit,2500.00,2027-03-16 08:30
S07,2027-03-16T14:00:00Z,270312,PPD,credit,2500.00,2027-03-16 13:00
S08,2027-03-16T14:00:00Z,,PPD,credit,2500.00,2027-03-16 13:00
S09,2027-03-16T14:00:00Z,270316,CCD,credit,1000000.01,2027-03-17 08:30
S10,2027-03-16T14:00:00Z,270316,IAT,credit,2500.00,2027-03-17 08:30
S11,2027-03-17T05:30:00Z,270317,PPD,credit,2500.00,2027-03-17 08:30
S12,2027-03-16T14:00:00Z,270318,PPD,debit,2500.00,returned
S13,2027-03-16T14:00:00Z,270318,PPD,credit,2500.00,2027-03-18 08:30
S14,2027-07-02T21:00:00Z,270705,PPD,credit,2500.00,2027-07-06 08:30
S15,2027-06-18T14:00:00Z,270618,PPD,credit,2500.00,2027-06-18 13:00
S16,2027-03-13T15:00:00Z,270315,PPD,credit,2500.00,confirm-with-odfi
S17,2027-03-16T14:00:00Z,27031X,PPD,credit,2500.00,2027-03-16 13:00
S18,2027-03-16T14:00:00Z,270316,PPD,credit,12.5O,error3. Compute the expected settlement with zoneinfo
The reference model reads settlement_matrix.csv and prints PASS or FAIL per row. It needs Python 3.10 or later; on Windows, install the tzdata package so zoneinfo finds America/New_York.
"""Expected FedACH settlement for each row of settlement_matrix.csv.
Rules: FedACH Processing Schedule (effective 12 September 2022) and Operating
Circular 4, Appendix B (effective 5 January 2026). Replace FED_CLOSED_2027 and
ODFI_LEAD with the values from your own calendar source and ODFI agreement.
"""
import csv
import sys
from datetime import date, datetime, time, timedelta
from decimal import Decimal, InvalidOperation
from zoneinfo import ZoneInfo
ET = ZoneInfo("America/New_York")
# Federal Reserve holiday schedule 2027, weekday closures only.
# 19 June and 25 December fall on Saturday: Reserve Banks open the Friday before.
# 4 July falls on Sunday: closed Monday 5 July.
FED_CLOSED_2027 = {
date(2027, 1, 1), date(2027, 1, 18), date(2027, 2, 15), date(2027, 5, 31),
date(2027, 7, 5), date(2027, 9, 6), date(2027, 10, 11), date(2027, 11, 11),
date(2027, 11, 25),
}
# Same Day windows: transmission deadline (ET) -> settlement time (ET).
SAME_DAY_WINDOWS = [(time(10, 30), time(13, 0)), (time(14, 45), time(17, 0)), (time(16, 45), time(18, 0))]
FUTURE_SETTLEMENT = time(8, 30)
BANKING_DAY_START = time(3, 0) # FedACH banking day: 3:00 a.m. to 2:59 a.m. ET
SAME_DAY_LIMIT = Decimal("1000000.00") # per entry, until 17 September 2027
ODFI_LEAD = timedelta(minutes=0) # your ODFI deadline = FedACH deadline minus this
def is_banking_day(d: date) -> bool:
return d.weekday() < 5 and d not in FED_CLOSED_2027
def next_banking_day(d: date) -> date:
d += timedelta(days=1)
while not is_banking_day(d):
d += timedelta(days=1)
return d
def banking_days_between(start: date, end: date) -> int:
count, d = 0, start
while d < end:
d = next_banking_day(d)
count += 1
return count
def parse_effective(raw: str) -> date | None:
"""YYMMDD from batch header positions 70-75. Blank or invalid returns None."""
try:
return datetime.strptime(raw.strip(), "%y%m%d").date()
except ValueError:
return None
def expected_settlement(row: dict) -> str:
submitted = datetime.fromisoformat(row["submitted_utc"].replace("Z", "+00:00")).astimezone(ET)
amount = Decimal(row["amount_usd"])
# Banking day of receipt: before 3:00 a.m. ET the file belongs to the previous calendar day.
receipt = submitted.date() if submitted.time() >= BANKING_DAY_START else submitted.date() - timedelta(days=1)
if not is_banking_day(receipt):
return "confirm-with-odfi" # Appendix B: other times apply around weekends and holidays
effective = parse_effective(row["effective_entry_date"])
same_day_date = effective is None or effective <= receipt
eligible = same_day_date and row["sec_code"] not in ("IAT", "ENR") and amount <= SAME_DAY_LIMIT
if eligible and submitted.date() == receipt:
for deadline, settles in SAME_DAY_WINDOWS:
cutoff = datetime.combine(receipt, deadline, tzinfo=ET) - ODFI_LEAD
if submitted <= cutoff:
return f"{receipt} {settles:%H:%M}"
if same_day_date:
return f"{next_banking_day(receipt)} {FUTURE_SETTLEMENT:%H:%M}"
target = effective if is_banking_day(effective) else next_banking_day(effective)
window = 1 if row["direction"] == "debit" else 2
if banking_days_between(receipt, target) > window:
return "returned"
return f"{target} {FUTURE_SETTLEMENT:%H:%M}"
def main(path: str) -> int:
failures = 0
with open(path, newline="", encoding="utf-8") as handle:
for row in csv.DictReader(handle):
try:
got = expected_settlement(row)
except (ValueError, KeyError, InvalidOperation) as exc:
got = "error"
print(f"{row.get('case_id', '?')}: input rejected: {exc!r}")
ok = got == row["expected"]
failures += not ok
print(f"{row['case_id']:4} {'PASS' if ok else 'FAIL'} expected={row['expected']:18} got={got}")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "settlement_matrix.csv"))Against the matrix above, all 18 rows pass. Row S18 carries a letter O in the amount; Decimal raises InvalidOperation, which is not a ValueError, so the except clause names it. S17 carries an invalid date, which the Federal Reserve FAQ treats as same-day eligible. S16 is a Saturday receipt: Appendix B says other times apply around weekends, so the model returns a marker to settle with the ODFI (originating bank) instead of guessing.
4. Assert the product against the model
Feed the product the same rows through its scheduler with a frozen clock, read back the settlement date it assigns and the value date it posts, and compare with the model output. A mismatch on S01 or S04 is a scheduler that stores the deadline as a UTC offset. A mismatch on S07 is a product that believes a past date means next day. Settlement-side checks against the bank file belong in the ledger reconciliation guide.
5. Check every scheduler time zone against both clock changes
The US switches at 2:00 a.m. local time on the second Sunday of March and the first Sunday of November (15 U.S.C. 260a). The EU switches on the last Sunday of March and of October (European Commission, Directive 2000/84/EC). A job scheduled in Berlin local time sees the FedACH deadline move twice a year:
from datetime import date, datetime, time
from zoneinfo import ZoneInfo
ET, BERLIN, UTC = ZoneInfo("America/New_York"), ZoneInfo("Europe/Berlin"), ZoneInfo("UTC")
for day in (date(2027, 3, 12), date(2027, 3, 15), date(2027, 3, 29), date(2027, 11, 1), date(2027, 11, 8)):
cutoff = datetime.combine(day, time(10, 30), tzinfo=ET)
print(day, cutoff.astimezone(UTC).strftime("%H:%M UTC"), cutoff.astimezone(BERLIN).strftime("%H:%M %Z"))2027-03-12 15:30 UTC 16:30 CET
2027-03-15 14:30 UTC 15:30 CET
2027-03-29 14:30 UTC 16:30 CEST
2027-11-01 14:30 UTC 15:30 CET
2027-11-08 15:30 UTC 16:30 CETBetween 14 and 28 March 2027 and between 31 October and 7 November 2027, the gap between ET and Berlin is five hours instead of six. A Berlin job set to 16:25 local reaches FedACH at 11:25 a.m. ET in those weeks and misses the first window.
Pitfalls outside vendor documentation
The banking day starts at 3:00 a.m. ET. Row S11 arrives at 1:30 a.m. ET on Wednesday 17 March 2027 dated for that Wednesday. By calendar date the file looks same-day and due at 1:00 p.m. Under Appendix B it belongs to Tuesday's banking day, so the effective date is one banking day ahead and the credit settles at 8:30 a.m. ET on Wednesday. A product that assigns receipt by calendar date predicts 1:00 p.m. settlement and a Same Day Entry Fee; neither happens.
A past, blank, or invalid date settles same day. Rows S07, S08, and S17 settle same day because each is $2,500.00, not IAT or ENR, and received before 4:45 p.m. ET. A batch built on Tuesday evening with Tuesday's date and released after 3:00 a.m. ET on Wednesday is stale-dated. Released before 4:45 p.m. ET, it settles same day under the same conditions and carries the Same Day Entry Fee.
Clock changes shift weekday offsets. US clocks change at 2:00 a.m. on a Sunday, outside the weekday same-day windows. The failures we test for are offsets: the weeks when US and EU clocks disagree, and schedulers that store the deadline in UTC.
Instant rails change business date while they run. The TARGET operating schedule states that TIPS changes its business day to the next business day following the closure of CLM (Central Liquidity Management) and RTGS (real-time gross settlement). In our reading, an SCT Inst (SEPA Instant Credit Transfer) payment settled after that closure on Friday, or on Saturday, carries Monday's business date while its timestamp shows the calendar day. A ledger that groups instant payments by timestamp date and statements that group them by business day will not match on those days. Confirm in the bank's camt.053 implementation guide which date it puts in <BookgDt>.
Test matrix
| Case | Boundary | Expected settlement (ET) |
|---|---|---|
| S01, S02 | 10:30 a.m. deadline minus and plus one minute, winter time | 1:00 p.m. and 5:00 p.m. on 12 March 2027 |
| S03, S04 | Same ET minute as S01 (one hour earlier in UTC), and same UTC minute as S01, first Monday of US summer time | 1:00 p.m. and 5:00 p.m. on 15 March 2027 |
| S05, S06 | 4:45 p.m. deadline minus and plus one minute | 6:00 p.m. on 15 March 2027; 8:30 a.m. on 16 March 2027 |
| S07, S08, S17 | Stale, blank, and invalid effective entry date | 1:00 p.m. same day |
| S09, S10 | Amount over $1,000,000; IAT entry | 8:30 a.m. next banking day |
| S11 | 1:30 a.m. receipt dated the calendar day | 8:30 a.m. that calendar day |
| S12, S13 | Effective date two banking days ahead | Debit returned; credit settles on that date |
| S14 | Effective date on a Monday Reserve Bank closure (5 July 2027) | 8:30 a.m. on Tuesday 6 July 2027 |
| S15 | Friday before a Saturday holiday (18 June 2027) | 1:00 p.m. same day |
| S16 | Saturday receipt | Confirm with ODFI |
| S18 | Malformed amount | Rejected with a reason, run continues |
Run the matrix once per scheduler time zone the product uses, and again for the Berlin weeks listed in step 5. We run it as part of payment rails testing, together with the return windows in the Nacha return code guide.
Related
Segments
Guides
Procedure on h2check.org
Questions
What is same day ACH?
What is the cutoff time for ACH?
What is the same day ACH limit?
Scope your testing
Send FintechAssuranceLabs your product, segment, money flows, and the standards that need evidence, and get a proposed test scope with its deliverables.
Scope your testing