Get Started Free

MongoDB date range query

Query MongoDB for documents between two dates with $gte and $lt, find the last 7 days, handle time zones, and fix dates that were stored as strings.

Updated October 28, 2026 ยท Tested on MongoDB 7.0, mongosh 2, PyMongo 4.19

Quick Answer

Use $gte for the start and $lt for the day after the end. This returns everything in October 2026:

db.events.find({
  createdAt: {
    $gte: ISODate('2026-10-01'),
    $lt:  ISODate('2026-11-01')
  }
})

This only works if createdAt is stored as a real BSON date, not a string. If it returns nothing, see dates stored as strings.

Why $lt the next day, not $lte the last day

ISODate('2026-10-31') means midnight at the start of October 31. So $lte on it silently drops almost the whole last day:

FilterIncludes 2026-10-31 at 18:00?
{ $gte: ISODate('2026-10-01'), $lte: ISODate('2026-10-31') }No
{ $gte: ISODate('2026-10-01'), $lt: ISODate('2026-11-01') }Yes

A half-open range ($gte start, $lt end) is also easy to chain: the end of one month is the start of the next, with no gaps or overlaps.

Greater than or before a date

// on or after October 1
db.events.find({ createdAt: { $gte: ISODate('2026-10-01') } })

// before October 1
db.events.find({ createdAt: { $lt: ISODate('2026-10-01') } })

// new Date() works the same as ISODate() in mongosh
db.events.find({ createdAt: { $gte: new Date('2026-10-01') } })

Dates without a time are read as UTC midnight. Add a time and offset when you need one: ISODate('2026-10-01T09:00:00-04:00').

Last 7 days, today, and other relative ranges

// last 7 days, computed in the shell
db.events.find({
  createdAt: { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
})

// last 7 days, computed on the server
db.events.find({
  $expr: {
    $gte: ['$createdAt', { $dateSubtract: { startDate: '$$NOW', unit: 'day', amount: 7 } }]
  }
})

Prefer the first form. A plain range on createdAt can use an index; the $expr form is handy in saved views and pipelines where you can't compute the date in code.

Time zones

MongoDB stores dates in UTC. "Everything that happened on September 30 in New York" is a different range than September 30 UTC. The fast way is to compute the UTC boundaries in your code. EDT is UTCโˆ’4, so:

db.events.find({
  createdAt: {
    $gte: ISODate('2026-09-30T04:00:00Z'),
    $lt:  ISODate('2026-10-01T04:00:00Z')
  }
})

If you need MongoDB to apply the time zone, aggregation date operators accept one, such as $dateToString: { date: '$createdAt', format: '%Y-%m-%d', timezone: 'America/New_York' }. That can't use an index, so filter by a wider UTC range first.

Dates stored as strings

If a range query returns nothing, check the field type. A date imported from JSON or CSV is often a string, and ISODate comparisons never match strings:

db.events.countDocuments({ createdAt: { $type: 'string' } })
db.events.countDocuments({ createdAt: { $type: 'date' } })

Convert them once with a pipeline update:

db.events.updateMany(
  { createdAt: { $type: 'string' } },
  [{ $set: { createdAt: { $toDate: '$createdAt' } } }]
)

$toDate fails on strings it can't parse and stops the update partway. Test it on a few documents first, or use $dateFromString with onError to handle bad values.

What we found testing this: silent misses

None of these produce an error. They just return fewer documents than you expect. We reproduced each one on MongoDB 7.0 with mongosh and PyMongo, with the machine set to New York time.

  • A date without Z is local time. new Date('2026-10-01') gives midnight UTC, but new Date('2026-10-01T00:00:00') gave 2026-10-01T04:00:00.000Z, which is midnight in New York. The same applies to new Date(2026, 9, 1). ISODate('2026-10-01T00:00:00') stayed UTC, so the two shell helpers disagree. mongosh runs on Node.js and this is standard JavaScript date parsing, so Node.js code behaves the same, and a range built from these misses the first four hours of the day on a server set to UTC. Always end ISO strings with Z or an offset.
  • Mixed types are skipped, not converted. In a collection holding three BSON dates, two strings and one epoch number (1790812800000), the October range matched 3 of 6. Run the $type counts above before trusting a date report.
  • String ranges break on unpadded dates. { $gte: '2026-10-01', $lt: '2026-10-02' } skipped the string '2026-10-1', because strings compare character by character and '1' sorts after '0'. $dateFromString parsed it correctly as 2026-10-01T00:00:00Z, so convert first.
  • datetime.now() in Python is off by your UTC offset. PyMongo stores a naive datetime as if it were UTC. Saved at 07:51 UTC from a New York machine, it came back as 03:51. Use datetime.now(timezone.utc). Reads are naive too unless you create the client with tz_aware=True.
  • UTC midnight is still "yesterday" in the Americas. Documents at 2026-10-01T00:00Z and 03:30Z both reported their New York day as 2026-09-30 via $dateToString: { timezone: 'America/New_York' }. That's why a "daily" chart can disagree with a UTC range query.
db.events.aggregate([
  { $group: { _id: { $type: '$createdAt' }, n: { $sum: 1 } } }
])
// [ { _id: 'date', n: 3 }, { _id: 'string', n: 2 }, { _id: 'double', n: 1 } ]

From Node.js and Python

const start = new Date('2026-10-01T00:00:00Z')
const end = new Date('2026-11-01T00:00:00Z')

const events = await db.collection('events')
  .find({ createdAt: { $gte: start, $lt: end } })
  .sort({ createdAt: 1 })
  .toArray()
from datetime import datetime, timezone

start = datetime(2026, 10, 1, tzinfo=timezone.utc)
end = datetime(2026, 11, 1, tzinfo=timezone.utc)

events = db.events.find({"createdAt": {"$gte": start, "$lt": end}}).sort("createdAt", 1)

Pass time zone-aware datetimes in Python. A naive one is treated as UTC, as shown above.

Make it fast

Index the date field, or put it after your equality fields in a compound index:

db.events.createIndex({ createdAt: 1 })
db.events.createIndex({ userId: 1, createdAt: -1 })  // "this user's recent events"

If you don't have a date field, the default _id ObjectId starts with its creation time, so { _id: { $gte: ObjectId.createFromTime(ISODate('2026-10-01').getTime() / 1000) } } works as a date range too. The free ObjectId decoder builds that boundary ObjectId for any date.

Frequently Asked Questions

Why does my MongoDB date query return no results?

Usually the field is a string, not a date. Check with { field: { $type: "string" } } and convert with $toDate.

How do I query between two dates in MongoDB?

Use { field: { $gte: startDate, $lt: dayAfterEndDate } }. Using $lt on the next day avoids missing records from the last day.

Is ISODate the same as new Date in mongosh?

Both create a BSON date, but they parse strings without a Z differently: ISODate treats them as UTC, while new Date("2026-10-01T00:00:00") uses the machine's local time zone.

Does MongoDB store dates in UTC?

Yes. BSON dates are milliseconds since the Unix epoch in UTC. Time zones are applied when you display or group them.

VisuaLeaf

Pick dates instead of typing ISODate

In the VisuaLeaf visual query builder, the value input adapts to the field type, so date fields get a date input. Add $gt and $lt conditions and the MongoDB filter is written for you.