Eloquent has several ways to filter by date, and picking the wrong one is a common source of subtle bugs — usually an off-by-one-day error caused by timezone handling, not the query method itself.
Exact date match: whereDate()
Use whereDate() when you want every row on a specific calendar date, regardless of the time portion:
Order::whereDate('created_at', '2026-09-01')->get();
This is equivalent to DATE(created_at) = '2026-09-01' at the SQL level, and correctly ignores the time component — you don't need to build a start/end-of-day range yourself for this case.
A date range: whereBetween()
Order::whereBetween('created_at', [
now()->subDays(7)->startOfDay(),
now()->endOfDay(),
])->get();
For ranges, pass full Carbon instances rather than date-only strings — whereBetween does a straightforward comparison, so a bare '2026-09-01' string is treated as midnight, silently excluding everything from later that same day.
Relative comparisons
Order::where('created_at', '>=', now()->subDays(30))->get();
The timezone bug
If created_at is stored in UTC (Laravel's default) but your application's display timezone is different, whereDate('created_at', today()) compares against UTC "today," which can be a different calendar day than the user's local "today" for several hours around midnight. Two fixes: convert the boundary to UTC explicitly before querying, or store and compare consistently in UTC and only convert to local time for display, never for the query itself.
$startOfDayUtc = now('Europe/Kyiv')->startOfDay()->setTimezone('UTC');
$endOfDayUtc = now('Europe/Kyiv')->endOfDay()->setTimezone('UTC');
Order::whereBetween('created_at', [$startOfDayUtc, $endOfDayUtc])->get();
Use half-open ranges when precision and indexes matter
For a calendar day on a timestamp column, another robust pattern is a half-open range: greater than or equal to the beginning of the day, and strictly less than the beginning of the next day. It avoids depending on an "end of day" fractional-second precision and keeps the comparison on the raw column:
$start = Carbon::parse('2026-09-01', 'Europe/Kyiv')
->startOfDay()
->utc();
$end = $start->copy()->addDay();
Order::where('created_at', '>=', $start)
->where('created_at', '<', $end)
->get();
This form is especially useful on large tables because wrapping a datetime column in a database function, as whereDate() commonly does, can make an otherwise useful index harder for the database optimizer to use efficiently.
Do not mix user timezone boundaries with server timezone assumptions
The question "orders from September 1" is incomplete until you know whose September 1. A reporting screen for one business may use the application's configured timezone; a multi-tenant or user-facing report may need the user's timezone. Build the local boundary first, then convert the boundary to the storage timezone for the query. Converting each database row to local time inside SQL is usually both more complicated and less index-friendly.
Date comparisons and immutable Carbon objects
Carbon date methods mutate a mutable Carbon instance unless you copy it first. If the same boundary variable is reused later, chaining startOfDay(), endOfDay(), or addDay() can quietly change the value another part of the method expects. Use copy() or CarbonImmutable when a query builds several related boundaries from one source date; it removes an entire class of accidental date-state bugs.
Pick the method that matches the question
Use whereDate() for a small, clearly calendar-based comparison; use raw timestamp boundaries when timezone conversion, index use, or exact inclusivity matters. The methods are not competing shortcuts — they encode different assumptions about the time component.