The one-line answer: reach for Tempo whenever time is a span. Reach for the Elixir standard library (Date, Time, DateTime, NaiveDateTime) whenever time is a single instant and a single operation — timestamps, benchmarks, quick comparisons, and integration with systems that speak POSIX seconds.

Interop between the two is deliberate and cheap. Convert at the boundary; use whichever idiom fits the task.

Setup — required for every example

Every code example in this guide uses the ~o sigil from Tempo.Sigils. Before running any of them — in iex, a script, or a module — you must bring the sigil into scope:

import Tempo.Sigils

The import adds only sigil_o/2 and sigil_TEMPO/2 to the caller's namespace; no helper functions leak in.

Tempo is the right call when…

You're thinking in intervals

Scheduling ("this meeting from 2pm to 3pm"), availability ("free during work hours Mon–Fri"), date ranges ("Q3 2026" — a span, not a moment), recurrence ("every second Monday of the month" — infinite span needing bounded materialisation). The stdlib models these as awkward pairs of DateTime values and hand-rolled comparison logic. Tempo models them as first-class bounded spans with predicates and operators.

You need set operations on time

Tempo.union/2, intersection/2, difference/2, symmetric_difference/2, complement/2. "Find the overlap between these two people's calendars." "Available slots within the work day minus busy periods." "Free hours across a team of five." None of this exists in the stdlib.

You need Allen's interval algebra

The thirteen named relations — :precedes, :meets, :overlaps, :during, :equals, and the eight inverses — exposed as Tempo.before?/2, after?/2, meets?/2, adjacent?/2, during?/2, within?/2, overlaps?/2. When "A comes before B" isn't specific enough ("is there a gap? does A end where B begins? do they overlap?"), stdlib's three-valued :lt | :eq | :gt doesn't help.

You need calendar awareness beyond Gregorian

Hebrew, Islamic, Julian, Coptic, Ethiopic, Persian, Chinese, Japanese, and more. ~o"5786-10-30[u-ca=hebrew]" and ~o"2026-06-15" can be compared, enumerated, and combined directly — Tempo performs the calendar conversion automatically. Historical / archaeological dates (the switch from Julian to Gregorian is a real thing that happens inside date comparisons before 1582) are first-class.

You need RRULE or iCalendar integration

Stdlib cannot model recurrence at all. Tempo.RRule and Tempo.ICal parse, generate, and expand RFC 5545 rules, round-trip .ics files, and preserve metadata through interval operations.

You need uncertain or masked dates

ISO 8601-2 EDTF: ~o"156X" (the 1560s), ~o"2022?" (approximately 2022), ~o"2026~" (circa 2026). Tempo treats these as bounded, enumerable values you can compare and operate on. Stdlib has no vocabulary for uncertainty.

You need locale-aware display

Tempo.to_string(tempo, locale: "de", format: :long) goes through CLDR via Localize. Year and month values render as closed intervals ("Jan – Dec 2026") because they are intervals. Stdlib's Calendar.strftime/2 has no concept of this — it formats instants, in the developer's choice of pattern string, without locale-specific month or weekday names.

You need leap-second awareness

Tempo.Interval.spans_leap_second?/1, leap_seconds_spanned/1, duration(iv, leap_seconds: true). There have been 27 positive leap seconds since 1972; the POSIX abstraction hides them. No stdlib equivalent.

You want wall-clock + zone preservation for future events

~o"2030-03-01T08:00:00[Europe/Paris]" stores the wall time and the zone, and projects UTC on demand — so your event survives DST rule changes that happen between now and 2030. Stdlib's DateTime caches a frozen UTC offset, which becomes stale the moment Tzdata updates.

Elixir stdlib is the right call when…

You need sub-second precision

Tempo operates at second resolution today. Comparison and arithmetic below the second fail. For microsecond or millisecond precision — API latency measurement, audio/video timing, high-frequency trading — use DateTime with its microsecond field.

You need monotonic time

Benchmarks, timeouts, retries, rate limiting. System.monotonic_time/0,1 is the right tool — any wall-clock interval is wrong for this purpose, in any library. Tempo deliberately does not model monotonic time, and never will, because the correct stdlib primitive exists.

You're talking to a POSIX world

Unix timestamps in HTTP headers (Last-Modified, Set-Cookie), database columns of type timestamp or bigint storing seconds-since-epoch, log lines, sensor data, webhook payloads. Use DateTime.from_unix/1 / DateTime.to_unix/1 at the edge, then promote to Tempo if the next operation is interval-shaped. Tempo deliberately does not expose from_unix/to_unix because those functions are instant-valued and would undermine the "time is intervals" thesis; the two-step conversion is the feature.

You just need the current time, once

DateTime.utc_now/0 is fine for a single timestamp on a single log line or error. Tempo.utc_now/0 exists and is also fine — but for a single instant in a single location with no further operations on it, stdlib's surface is all you need.

You're at a system boundary talking to non-Tempo code

Ecto schemas with :utc_datetime columns — use DateTime. Phoenix controllers returning %DateTime{} to the client — use DateTime. Convert to Tempo when the next operation is interval-ish; stay in stdlib when it isn't. Premature conversion adds struct-traversal cost without benefit.

You're in a hot loop where allocation matters

Tempo values carry more fields than stdlib values (:calendar, :extended, :qualification, :qualifications). For high-frequency per-event work (log processing at millions of events per second, sensor streams), stdlib's lighter structs pay off. If you then need interval operations, promote after aggregation.

The interop contract

Conversion is one function call each way:

# Stdlib → Tempo
Tempo.from_date(~D[2026-06-15])
Tempo.from_time(~T[14:30:00])
Tempo.from_naive_date_time(~N[2026-06-15 14:30:00])
Tempo.from_date_time(~U[2026-06-15 14:30:00Z])
Tempo.from_elixir(value, resolution: :day)  # unified gateway

# Tempo → Stdlib
Tempo.to_date(tempo)
Tempo.to_time(tempo)
Tempo.to_naive_date_time(tempo)
Tempo.to_calendar(tempo)  # returns DateTime when possible, else NaiveDateTime or Date

A healthy Tempo-using application looks like this:

  • Edge layer (HTTP controllers, DB adapters, log parsing): stdlib. Parse Unix timestamps, DateTime fields, ISO 8601 strings into stdlib types and hand them to the domain layer.

  • Domain layer (scheduling, availability, recurrence, set operations, interval predicates): Tempo. This is where the "intervals, not instants" thesis pays off.

  • Display layer (UI rendering, reports, notifications): Tempo.to_string/2 for user-facing output via CLDR/Localize.

Convert at the edges; stay in Tempo wherever the business logic is interval-shaped.

The decision tree

Am I asking a question about a span? Yes → Tempo. No → stdlib is probably enough.

Do I need sub-second precision or monotonic time? Yes → stdlib. No → either works; prefer whichever idiom the surrounding code already uses.

Am I integrating with a POSIX system or a %DateTime{}-typed API? Yes → stdlib at the boundary, promote to Tempo if the next operation is interval-shaped.

Am I comparing values across different calendars, time zones for future dates, or uncertain / masked dates? Yes → Tempo. Stdlib can't represent these cleanly.