"We answer every ticket within 4 business hours." It's a promise that appears in thousands of support contracts — and it's surprisingly easy to calculate wrong.
A ticket opened on Friday at 15:00 with a 4-hour SLA is not due Friday at 19:00. If the team works 09:00–18:00 with a lunch break, it's due on Monday. If Monday is a holiday, it's due Tuesday. If the ticket arrives at 20:30, the clock shouldn't even start until the next morning.
Let's look at the rules, and then at code that gets them right.
The rules an SLA clock has to follow
- Only working time counts. Nights, weekends and holidays don't consume the SLA.
- Breaks don't count either. A lunch break between 12:00 and 13:00 pauses the clock.
- Tickets that arrive outside working hours start at the next opening.
- A deadline that fills the day exactly ends at closing time — 18:00 Friday, not 09:00 Monday.
- Hours can differ by weekday. Plenty of teams close early on Fridays.
Doing this with Date arithmetic quickly turns into a loop full of edge cases. I'll use duedates, a zero-dependency TypeScript library, to keep the examples short.
A support calendar
import { createCalendar } from '@contalert/duedates'; const support = createCalendar({ holidays: ['2026-12-25'], workingHours: [['09:00', '12:00'], ['13:00', '18:00']], // lunch break excluded }); Weekends default to Saturday and Sunday. Moments are plain wall-clock strings — YYYY-MM-DDTHH:mm — in the team's own time zone, so no server time zone can shift them.
Computing the deadline
// Friday 15:00 + 4 business hours support.addBusinessHours('2026-11-06T15:00', 4); // '2026-11-09T10:00' — 3 hours on Friday, 1 hour on Monday // Ticket arrives Friday 20:30, after closing support.addBusinessHours('2026-11-06T20:30', 2); // '2026-11-09T11:00' — the clock starts Monday 09:00 // Wednesday 11:30 + 1 hour, across lunch support.addBusinessHours('2026-11-11T11:30', 1); // '2026-11-11T13:30' // Christmas Eve 17:00 + 3 hours support.addBusinessHours('2026-12-24T17:00', 3); // '2026-12-28T11:00' — 1 hour on the 24th, then the holiday and the weekend are skipped Warning before the breach
A good support tool warns the agent before the deadline, not after. Negative values count backwards:
const deadline = support.addBusinessHours('2026-11-06T15:00', 4); // '2026-11-09T10:00' support.addBusinessHours(deadline, -1); // '2026-11-09T09:00' — one business hour before the deadline Notice that one business hour before Monday 10:00 is Monday 09:00 — not Sunday.
How much of the SLA has been used?
For dashboards and reports you need the opposite question: how many working minutes passed between two moments?
support.businessMinutesBetween('2026-11-06T15:00', '2026-11-09T10:30'); // 270 — 180 minutes on Friday, 90 on Monday support.isWithinBusinessHours('2026-11-09T12:30'); // false — lunch break Short Fridays
When hours differ by day, give each ISO weekday (1 = Monday … 7 = Sunday) its own ranges:
const shortFridays = createCalendar({ workingHours: { 1: [['09:00', '18:00']], 2: [['09:00', '18:00']], 3: [['09:00', '18:00']], 4: [['09:00', '18:00']], 5: [['09:00', '13:00']], }, }); shortFridays.addBusinessHours('2026-11-06T11:00', 4); // '2026-11-09T11:00' — 2 hours on Friday, 2 on Monday Getting "now" in the right time zone
Your server probably runs in UTC, while the support team works in New York or São Paulo. Ask for the team's wall clock explicitly:
import { now } from '@contalert/duedates'; const openedAt = now('America/New_York'); // e.g. '2026-11-06T15:00' const dueAt = support.addBusinessHours(openedAt, 4); Wrapping up
An SLA clock needs only a few concepts — working ranges, holidays and a direction to count — but each one hides edge cases. Keeping moments as plain strings and counting minutes inside working ranges avoids most of them.
duedates is MIT-licensed and has no dependencies:
npm install @contalert/duedates Next week in this series: why "add one day" breaks with JavaScript Date, time zones and daylight saving time.