DateDiff
Returns the difference between two dates in the requested unit. Runtime-proven on a live Marketing Cloud Engagement CloudPage — including the fact that it counts unit boundaries rather than elapsed time, so one minute either side of midnight is a whole day apart.
Syntax
DateDiff(startDate, endDate, unitOfDifference) → number
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
startDate |
string | date | Yes | The starting date, either a real date value or a parseable date string |
endDate |
string | date | Yes | The end date, either a real date value or a parseable date string |
unitOfDifference |
string | Yes | One of Y, M, D, H or MI, in any capitalisation. Nothing else is accepted |
Example
%%[
VAR @start, @end, @days
SET @start = "2026-01-10 08:00:00"
SET @end = "2026-01-11 07:00:00"
SET @days = DateDiff(@start, @end, "D")
]%%
%%=v(@days)=%%
Renders 1 — even though only 23 hours separate the two values, because the date changed once.
A later end date gives a positive number and an earlier one a negative number, which is what makes the result usable as a countdown or an overdue check:
%%[
VAR @due, @today, @remaining
SET @due = "2026-05-01 00:00:00"
SET @today = "2026-04-24 00:00:00"
SET @remaining = DateDiff(@today, @due, "D")
IF @remaining > 0 THEN
]%%
<p>%%=v(@remaining)=%% days left.</p>
%%[ ELSE ]%%
<p>Past due.</p>
%%[ ENDIF ]%%
The order of the two dates is the whole sign convention — swap them and you get the same magnitude negated.
Return value
number — a whole number of unit boundaries, negative when the end date precedes the start date and 0 when both dates fall inside the same unit. It is a real number, not text: Add(DateDiff(@s, @e, "H"), 1) returned 24 where the inner call returned 23.
There is no closed set of sentinel values to test for, and no failure value either. Every rejected argument aborts the page with HTTP 422 rather than returning an error token, so a result that exists is always a real count.
Behaviour
The five units are the whole list, and they ignore case. Y, M, D, H and MI all work; y, m, d, h, mi, Mi and mI each returned exactly what their upper-case spelling returned. There is no seconds unit, so MI is the finest resolution available.
Anything else costs you the entire page. A seconds token, a weeks token, a quarters token, a milliseconds token, the spelled-out word for a day, an unknown two-letter token and an empty token string each returned HTTP 422 with no output at all, while a control call in the same deployment rendered normally. Nothing is written when the page aborts, so a bad unit is not something the caller can detect and recover from.
A date string and a real date value are interchangeable, in either position independently. ISO, US slash and spelled-out month forms all parsed, and the output of Now() and of DateAdd went straight in with no conversion. Mixing a value with a string in one call works in both directions. But an unparseable string, an empty string or a plain number aborts the page in either position — the same unforgiving behaviour as DateAdd, and the opposite of FormatDate.
The result counts boundaries, not elapsed time
Both dates are truncated to the requested unit and then subtracted. What comes back is how many unit boundaries lie between them.
| Call | Renders |
|---|---|
DateDiff("2026-01-10 08:00:00", "2026-01-11 07:00:00", "D") |
1 |
DateDiff("2026-01-10 00:00:00", "2026-01-10 23:59:00", "D") |
0 |
DateDiff("2026-12-31 23:59:00", "2027-01-01 00:00:00", "Y") |
1 |
DateDiff("2026-01-01 00:00:00", "2026-12-31 23:59:00", "Y") |
0 |
DateDiff("2026-01-10 08:00:00", "2026-01-10 09:30:00", "H") |
1 |
DateDiff("2026-01-10 08:00:59", "2026-01-10 08:01:00", "MI") |
1 |
The first two rows are the pair worth remembering: 23 hours that cross midnight count as a day, and 23 hours that do not, count as nothing. The third row is the same rule at its most extreme — sixty seconds spanning New Year returns 1 for Y, M, D and MI simultaneously.
Anything finer than the requested unit is discarded rather than contributing a fraction: 59 seconds measured in MI gives 0, but one second across a minute boundary gives 1. So DateDiff(a, b, "D") == 1 means the date changed once, not that a day passed. See the differs-from-docs card for the full write-up.
Show test script
%%[
VAR @b, @s, @e, @p1, @p2
SET @b = RequestParameter("b")
SET @s = "2026-01-10 08:00:00"
SET @e = "2026-01-11 07:00:00"
SET @p1 = "2026-03-04 13:52:07"
SET @p2 = "2027-05-09 09:15:31"
/* the control - fetch this whenever a branch below renders nothing */
IF @b == "ctrl" THEN
OutputLine(Concat("--- ctrl start ---"))
OutputLine(Concat("CTRL=[", DateDiff("2026-01-10 08:00:00", "2026-01-11 08:00:00", "D"), "]"))
OutputLine(Concat("CTRLH=[", DateDiff(@s, @e, "H"), "]"))
OutputLine(Concat("--- ctrl done ---"))
ENDIF
/* the five accepted units, the sign convention, and a same-instant pair */
IF @b == "safe" THEN
OutputLine(Concat("--- safe start ---"))
OutputLine(Concat("SPAN_Y=[", DateDiff(@p1, @p2, "Y"), "]"))
OutputLine(Concat("SPAN_M=[", DateDiff(@p1, @p2, "M"), "]"))
OutputLine(Concat("SPAN_D=[", DateDiff(@p1, @p2, "D"), "]"))
OutputLine(Concat("SPAN_H=[", DateDiff(@p1, @p2, "H"), "]"))
OutputLine(Concat("SPAN_MI=[", DateDiff(@p1, @p2, "MI"), "]"))
OutputLine(Concat("FWD_H=[", DateDiff(@s, @e, "H"), "]"))
OutputLine(Concat("REV_H=[", DateDiff(@e, @s, "H"), "]"))
OutputLine(Concat("SAME_D=[", DateDiff(@s, @s, "D"), "]"))
OutputLine(Concat("ARITH=[", Add(DateDiff(@s, @e, "H"), 1), "]"))
OutputLine(Concat("--- safe done ---"))
ENDIF
/* partial intervals are neither rounded nor truncated */
IF @b == "trunc" THEN
OutputLine(Concat("--- trunc start ---"))
OutputLine(Concat("T23H_D=[", DateDiff(@s, @e, "D"), "]"))
OutputLine(Concat("T90MI_H=[", DateDiff("2026-01-10 08:00:00", "2026-01-10 09:30:00", "H"), "]"))
OutputLine(Concat("T59MI_H=[", DateDiff("2026-01-10 08:00:00", "2026-01-10 08:59:00", "H"), "]"))
OutputLine(Concat("T364D_Y=[", DateDiff("2026-01-10 08:00:00", "2027-01-09 08:00:00", "Y"), "]"))
OutputLine(Concat("T366D_Y=[", DateDiff("2026-01-10 08:00:00", "2027-01-11 08:00:00", "Y"), "]"))
OutputLine(Concat("T29D_M=[", DateDiff("2026-01-10 08:00:00", "2026-02-08 08:00:00", "M"), "]"))
OutputLine(Concat("TNEG23H_D=[", DateDiff(@e, @s, "D"), "]"))
OutputLine(Concat("TNEG90MI_H=[", DateDiff("2026-01-10 09:30:00", "2026-01-10 08:00:00", "H"), "]"))
OutputLine(Concat("TSEC_MI=[", DateDiff("2026-01-10 08:00:00", "2026-01-10 08:00:59", "MI"), "]"))
OutputLine(Concat("--- trunc done ---"))
ENDIF
/* the result counts boundaries of the requested unit */
IF @b == "bound" THEN
OutputLine(Concat("--- bound start ---"))
OutputLine(Concat("NYE_Y=[", DateDiff("2026-12-31 23:59:00", "2027-01-01 00:00:00", "Y"), "]"))
OutputLine(Concat("NYE_M=[", DateDiff("2026-12-31 23:59:00", "2027-01-01 00:00:00", "M"), "]"))
OutputLine(Concat("NYE_D=[", DateDiff("2026-12-31 23:59:00", "2027-01-01 00:00:00", "D"), "]"))
OutputLine(Concat("NYE_MI=[", DateDiff("2026-12-31 23:59:00", "2027-01-01 00:00:00", "MI"), "]"))
OutputLine(Concat("ALMOSTY=[", DateDiff("2026-01-01 00:00:00", "2026-12-31 23:59:00", "Y"), "]"))
OutputLine(Concat("ALMOSTM=[", DateDiff("2026-01-01 00:00:00", "2026-01-31 23:59:00", "M"), "]"))
OutputLine(Concat("ALMOSTD=[", DateDiff("2026-01-10 00:00:00", "2026-01-10 23:59:00", "D"), "]"))
OutputLine(Concat("SECIGN=[", DateDiff("2026-01-10 08:00:59", "2026-01-10 08:01:00", "MI"), "]"))
OutputLine(Concat("--- bound done ---"))
ENDIF
/* the unit token ignores case in every spelling */
IF @b == "case" THEN
OutputLine(Concat("--- case start ---"))
OutputLine(Concat("cY=[", DateDiff(@p1, @p2, "y"), "]"))
OutputLine(Concat("cM=[", DateDiff(@p1, @p2, "m"), "]"))
OutputLine(Concat("cD=[", DateDiff(@p1, @p2, "d"), "]"))
OutputLine(Concat("cH=[", DateDiff(@p1, @p2, "h"), "]"))
OutputLine(Concat("cMI=[", DateDiff(@p1, @p2, "mi"), "]"))
OutputLine(Concat("cMi=[", DateDiff(@p1, @p2, "Mi"), "]"))
OutputLine(Concat("cmI=[", DateDiff(@p1, @p2, "mI"), "]"))
OutputLine(Concat("--- case done ---"))
ENDIF
/* date strings and real date values are interchangeable in both positions */
IF @b == "dtype" THEN
OutputLine(Concat("--- dtype start ---"))
OutputLine(Concat("BOTHSTR=[", DateDiff("2026-01-10", "2026-01-12", "D"), "]"))
OutputLine(Concat("USSTR=[", DateDiff("1/10/2026", "1/12/2026", "D"), "]"))
OutputLine(Concat("WORDSTR=[", DateDiff("10 January 2026", "12 January 2026", "D"), "]"))
OutputLine(Concat("BOTHVAL=[", DateDiff(Now(), DateAdd(Now(), 2, "D"), "D"), "]"))
OutputLine(Concat("MIXVAL1=[", DateDiff(DateAdd("2026-01-10", 0, "D"), "2026-01-12", "D"), "]"))
OutputLine(Concat("MIXVAL2=[", DateDiff("2026-01-10", DateAdd("2026-01-10", 2, "D"), "D"), "]"))
OutputLine(Concat("--- dtype done ---"))
ENDIF
/* every branch below aborts the page - each renders NOTHING at all */
IF @b == "sec" THEN
OutputLine(Concat("--- sec start ---"))
OutputLine(Concat("SEC=[", DateDiff(@s, "2026-01-10 08:00:59", "S"), "]"))
ENDIF
IF @b == "wk" THEN
OutputLine(Concat("--- wk start ---"))
OutputLine(Concat("WK=[", DateDiff(@p1, @p2, "W"), "]"))
ENDIF
IF @b == "ms" THEN
OutputLine(Concat("--- ms start ---"))
OutputLine(Concat("MS=[", DateDiff(@p1, @p2, "MS"), "]"))
ENDIF
IF @b == "qt" THEN
OutputLine(Concat("--- qt start ---"))
OutputLine(Concat("QT=[", DateDiff(@p1, @p2, "Q"), "]"))
ENDIF
IF @b == "longd" THEN
OutputLine(Concat("--- longd start ---"))
OutputLine(Concat("LONGD=[", DateDiff(@p1, @p2, "day"), "]"))
ENDIF
IF @b == "bogus" THEN
OutputLine(Concat("--- bogus start ---"))
OutputLine(Concat("BOGUS=[", DateDiff(@p1, @p2, "ZZ"), "]"))
ENDIF
IF @b == "emptytok" THEN
OutputLine(Concat("--- emptytok start ---"))
OutputLine(Concat("EMPTYTOK=[", DateDiff(@p1, @p2, ""), "]"))
ENDIF
IF @b == "baddate" THEN
OutputLine(Concat("--- baddate start ---"))
OutputLine(Concat("BADDATE=[", DateDiff("not a date at all", @p2, "D"), "]"))
ENDIF
IF @b == "baddate2" THEN
OutputLine(Concat("--- baddate2 start ---"))
OutputLine(Concat("BADDATE2=[", DateDiff(@p1, "not a date at all", "D"), "]"))
ENDIF
IF @b == "emptydate" THEN
OutputLine(Concat("--- emptydate start ---"))
OutputLine(Concat("EMPTYDATE=[", DateDiff("", @p2, "D"), "]"))
ENDIF
IF @b == "numdate" THEN
OutputLine(Concat("--- numdate start ---"))
OutputLine(Concat("NUMDATE=[", DateDiff(20260304, @p2, "D"), "]"))
ENDIF
IF @b == "a2" THEN
OutputLine(Concat("--- a2 start ---"))
OutputLine(Concat("A2=[", DateDiff(@p1, @p2), "]"))
ENDIF
IF @b == "a4" THEN
OutputLine(Concat("--- a4 start ---"))
OutputLine(Concat("A4=[", DateDiff(@p1, @p2, "D", "extra"), "]"))
ENDIF
]%%
A bare string literal passed to OutputLine renders an empty line while the page still returns HTTP 200, so the marker silently vanishes and the block looks like a function that produced no output. Always wrap it — OutputLine(Concat("--- safe start ---")) — even for a single argument.
AMPscript has no try/catch, so a bad unit or an unreadable date aborts the render and throws away everything already written above it. When testing, put each risky call behind its own RequestParameter branch and fetch them one at a time — otherwise a single failure hides every result on the page.
Availability
| Platform | Available |
|---|---|
| Marketing Cloud Engagement | Yes |
| Marketing Cloud Next | Yes, from API 67.0 |
See also
- DateAdd — the inverse operation, sharing the same five-unit token set
- Now — the value most often passed in as one of the two dates
- FormatDate — formats a date, and swallows bad input where this function aborts
- The differs-from-docs cards — why a 23-hour gap can be one day
- Official reference · ampscript.guide