MongoDB $bucket Stage

Beginner
⏱️ 10 min read
📚 Updated: Jul 2026
🎯 5 Examples
Aggregation

What You’ll Learn

The $bucket stage groups documents into fixed numeric ranges you define—perfect for price tiers, age bands, score histograms, and any report where bucket edges are known in advance.

01

Range buckets

Define edges with boundaries.

02

groupBy field

Numeric expression per doc.

03

Custom output

count, avg, sum, $push.

04

default bucket

Catch outliers and nulls.

05

Histograms

One row per range band.

06

vs $bucketAuto

Fixed vs auto edges.

Definition and Usage

$bucket is an aggregation stage that categorizes documents by a numeric groupBy value into predefined intervals. Instead of writing manual $switch or multiple $match stages, you list boundary numbers and MongoDB assigns each document to the correct bucket.

💡
Beginner tip

Each bucket’s _id in the output is the lower bound of that range (e.g. boundary 18 means “18 and up until the next boundary”). Think of it like chart bins on a histogram.

Use $bucket when business rules define the ranges—$0–$49, $50–$99, $100+. Use $bucketAuto when you want MongoDB to calculate evenly spaced edges from the data. Pair accumulators like $sum and $avg inside output.

📝 Syntax

$bucket requires groupBy and boundaries; other fields customize behavior:

mongosh
{
  $bucket: {
    groupBy: <expression>,
    boundaries: [ <num1>, <num2>, ... ],
    default: <literal>,
    output: {
      <field1>: { <accumulator expression> },
      ...
    }
  }
}

Syntax Rules

  • groupBy — required; expression whose numeric result determines the bucket (usually a field path like "$price").
  • boundaries — required; ascending array with at least two values. Each value is a bucket’s inclusive lower bound.
  • Range logic — value v falls in bucket i when v >= boundaries[i] and v < boundaries[i+1]. Last bucket: v >= boundaries[last].
  • default — optional; bucket for values below the first boundary, above the last (when capped), null, or missing.
  • output — optional; accumulators per bucket. Default is { count: { $sum: 1 } } when omitted.
  • One doc per bucket — output is a small set of summary documents, not one row per input document.
mongosh
db.products.aggregate([
  {
    $bucket: {
      groupBy: "$price",
      boundaries: [0, 50, 100, 200],
      default: "Other",
      output: { count: { $sum: 1 } }
    }
  }
]);

⚡ Quick Reference

QuestionAnswer
Stage typePipeline grouping by fixed numeric ranges
RequiredgroupBy, boundaries (≥ 2 values, ascending)
Bucket _idLower bound of the range (or default value)
Default output{ count: { $sum: 1 } }
vs $group$bucket = range bins; $group = any _id expression
vs $bucketAuto$bucket = you pick edges; $bucketAuto = auto-spaced
Price tiers
boundaries: [0, 25, 50, 100]
groupBy: "$price"

Retail bands

Age groups
boundaries: [0, 13, 18, 65]
groupBy: "$age"

Demographics

Avg per bucket
output: {
  count: { $sum: 1 },
  avgScore: { $avg: "$score" }
}

Custom stats

Catch-all
default: "Out of range"

Null / edge cases

🧰 Parameters

Every key inside the $bucket object controls bucketing behavior:

groupBy Required

Expression evaluated per document. Must resolve to a number (or date converted to number) for boundary matching.

"$price"
{ $subtract: ["$year", 2000] }
boundaries Required

Sorted array of at least two numbers. Defines bucket lower bounds. Gaps between consecutive values form each bin.

[0, 50, 100, 500]
→ [0,50), [50,100), [100,500), [500,∞)
default Optional

Literal value for the catch-all bucket. Documents outside defined ranges or with null/missing groupBy use this bucket.

default: "Other"
_id becomes "Other"
output Optional

Accumulator expressions computed per bucket—same style as $group fields.

count: { $sum: 1 }
total: { $sum: "$amount" }

Examples Gallery

Sample product catalog—build price histograms, score bands with averages, handle outliers, and compare with manual grouping.

📚 Getting Started

Sample products and a basic price-tier histogram.

Example 1 — Sample products collection

mongosh
db.products.insertMany([
  { name: "Pen",       price: 2,   score: 78 },
  { name: "Notebook",  price: 8,   score: 85 },
  { name: "Headphones", price: 45, score: 91 },
  { name: "Keyboard",  price: 75,  score: 88 },
  { name: "Monitor",   price: 220, score: 94 },
  { name: "Laptop",    price: 899, score: 96 },
  { name: "Gift card", price: null, score: 70 }
]);

How It Works

Seven products with varied prices and review scores—includes a null price to demo the default bucket later.

Example 2 — Price histogram with count

mongosh
db.products.aggregate([
  {
    $bucket: {
      groupBy: "$price",
      boundaries: [0, 25, 100, 500],
      default: "Unpriced",
      output: { count: { $sum: 1 } }
    }
  },
  { $sort: { _id: 1 } }
]);

/* _id: 0   → count: 2  (Pen $2, Notebook $8)     [0, 25)
   _id: 25  → count: 2  (Headphones $45, Keyboard $75) [25, 100)
   _id: 100 → count: 1  (Monitor $220)            [100, 500)
   _id: 500 → count: 1  (Laptop $899)             [500, ∞)
   _id: "Unpriced" → count: 1 (Gift card, null) */
*/

How It Works

MongoDB evaluates $price for each document, finds the matching interval, and increments count in that bucket. Sort by _id for readable chart order.

📈 Practical Patterns

Richer output, default handling, and comparison with $group.

Example 3 — Average score per price band

mongosh
db.products.aggregate([
  {
    $bucket: {
      groupBy: "$price",
      boundaries: [0, 50, 150, 1000],
      default: "Unpriced",
      output: {
        count: { $sum: 1 },
        avgScore: { $avg: "$score" },
        items: { $push: "$name" }
      }
    }
  }
]);

/* _id: 0   → count: 2, avgScore: 81.5, items: ["Pen","Notebook"]
   _id: 50  → count: 2, avgScore: 89.5, items: ["Headphones","Keyboard"]
   _id: 150 → count: 2, avgScore: 95,   items: ["Monitor","Laptop"] */
*/

How It Works

The output object works like $group fields—mix $sum, $avg, and $push to build dashboard-ready bucket summaries.

Example 4 — Filter first, then bucket scores

mongosh
// Only priced items — bucket review scores
db.products.aggregate([
  { $match: { price: { $type: "number" } } },
  {
    $bucket: {
      groupBy: "$score",
      boundaries: [0, 80, 90, 100],
      default: "Unknown",
      output: {
        count: { $sum: 1 },
        maxPrice: { $max: "$price" }
      }
    }
  },
  { $sort: { _id: 1 } }
]);

/* _id: 0  → scores [0, 80)   — Pen (78)
   _id: 80 → scores [80, 90)  — Notebook, Keyboard
   _id: 90 → scores [90, 100) — Headphones, Monitor, Laptop */
*/

How It Works

Place $match before $bucket to exclude documents you do not want counted. Here we skip the null-price gift card, then bucket on $score instead of price.

Example 5 — $bucket vs manual $group ranges

mongosh
// $bucket — concise fixed ranges
db.products.aggregate([
  {
    $bucket: {
      groupBy: "$price",
      boundaries: [0, 50, 200],
      default: "Other",
      output: { count: { $sum: 1 } }
    }
  }
]);

// Manual $group with $switch — same idea, more verbose
db.products.aggregate([
  {
    $group: {
      _id: {
        $switch: {
          branches: [
            { case: { $lt: ["$price", 50] }, then: 0 },
            { case: { $lt: ["$price", 200] }, then: 50 }
          ],
          default: "Other"
        }
      },
      count: { $sum: 1 }
    }
  }
]);

How It Works

When ranges are numeric and fixed, $bucket replaces boilerplate $switch logic. Use $group when _id is categorical (country, status) rather than numeric bins.

🧠 How $bucket Works

1

Evaluate groupBy

For each input document, MongoDB computes the groupBy expression (e.g. $price).

Input
2

Find matching bucket

Compare the value against boundaries. Assign to the interval or the default bucket if out of range.

Bin
3

Run accumulators

Update output fields for that bucket—$sum, $avg, $push, etc.

Aggregate
=

One doc per bucket

Pipeline outputs summary rows with _id = lower bound (or default label).

📝 Notes

  • boundaries must be in strict ascending order—duplicate or descending values cause errors.
  • Bucket _id is the lower bound number, not a human label—use $addFields after bucketing to add friendly names like “$0–$49”.
  • Without default, documents outside all ranges may be dropped from the result.
  • groupBy on dates works when boundaries are numeric timestamps or when you convert dates first with $toLong or similar.
  • Empty buckets (no matching documents) are omitted from output—they do not appear with count 0.
  • Previous topic: $addFields. Next: $bucketAuto.

Conclusion

$bucket turns numeric fields into histogram-style summaries with minimal code. Define groupBy and boundaries, add output accumulators for richer stats, and set default for edge cases.

When bucket edges should come from the data itself, switch to $bucketAuto. When grouping is not range-based, use $group directly.

💡 Best Practices

✅ Do

  • Always set default when null or out-of-range values are possible
  • Put $match before $bucket to reduce documents processed
  • Sort output by _id for charts that read left-to-right by range
  • Use meaningful boundary edges aligned to business rules (tax brackets, tiers)
  • Add a follow-up $addFields stage for human-readable bucket labels

❌ Don’t

  • Use $bucket for non-numeric categories—use $group instead
  • Forget that the last bucket has no upper cap unless you rely on default
  • Expect zero-count buckets to appear—they are skipped in output
  • Mix unsorted or duplicate boundary values
  • Confuse pipeline $bucket with unrelated “bucket” terms (S3, etc.)

Key Takeaways

Knowledge Unlocked

Five things to remember about $bucket

Use these points whenever you build range-based reports.

5
Core concepts
📈 02

groupBy

Numeric expression.

Required
📦 03

_id = lower bound

Bucket identifier.

Output
⚠️ 04

default bucket

Catch null/outliers.

Safety
🛠 05

Custom output

avg, sum, push.

Stats

❓ Frequently Asked Questions

$bucket groups documents into fixed numeric ranges (buckets) based on a groupBy expression. You define boundary values; MongoDB assigns each document to the bucket whose range contains its groupBy value and returns one summary document per bucket.
groupBy (expression to bucket on, e.g. "$price") and boundaries (array of at least two ascending numbers that define bucket lower bounds). default is optional but recommended for null or out-of-range values. output is optional—if omitted, MongoDB adds count: { $sum: 1 }.
Each boundary is the inclusive lower bound of a bucket. A document with groupBy value v goes into the bucket where v >= boundary[i] and v < boundary[i+1]. The last bucket includes all values >= the last boundary. Boundaries must be sorted ascending.
Documents whose groupBy value is less than the first boundary, greater than or equal to the last boundary (when you want to cap ranges), null, or non-numeric (when boundaries are numeric) can land in the default bucket. Set default: "Other" or similar; that value becomes the bucket _id.
$bucket uses boundaries you specify—ideal for known ranges like age bands 0–18, 18–65. $bucketAuto computes bucket edges from the data (groupBy + buckets count). Use $bucket for fixed business rules; $bucketAuto for exploratory histograms.
Yes. The output object accepts any accumulator expressions—count with $sum, average with $avg, lists with $push, totals with $sum on a field path. Each bucket gets one aggregated result document.
Did you know?

$bucket and $bucketAuto were introduced in MongoDB 3.4 as dedicated histogram stages—before that, developers built the same logic with $group and $switch. See the official $bucket docs.

Continue the Stages Series

Master fixed-range histograms with $bucket, then learn $bucketAuto for data-driven edges.

Next: $bucketAuto →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

5 people found this page helpful