ELK Stack — Aggregations

8 minute read

Introduction

Aggregations are Elasticsearch’s analytics engine. While queries find documents, aggregations summarize, group, and compute statistics over your data. They power the charts, histograms, and dashboards you see in Kibana.

Elasticsearch supports three types of aggregations:

┌─────────────────────────────────────────────────────────────┐
│                   Aggregation Types                         │
├───────────────────┬─────────────────────────────────────────┤
│  Metric           │ Compute numbers from field values       │
│  Aggregations     │ avg, sum, min, max, stats, cardinality  │
├───────────────────┼─────────────────────────────────────────┤
│  Bucket           │ Group documents into buckets            │
│  Aggregations     │ terms, date_histogram, range, filters   │
├───────────────────┼─────────────────────────────────────────┤
│  Pipeline         │ Compute over other aggregation results  │
│  Aggregations     │ derivative, moving_avg, cumulative_sum  │
└───────────────────┴─────────────────────────────────────────┘

The size: 0 Pattern

When you only need aggregation results (no individual documents), set size: 0 to skip the hits array entirely. This makes the query faster and the response smaller.

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "avg_response_time": {
      "avg": { "field": "response_time" }
    }
  }
}'
{
  "took": 2,
  "hits": {
    "total": { "value": 6, "relation": "eq" },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "avg_response_time": {
      "value": 968.6333312988281
    }
  }
}

Metric Aggregations

Metric aggregations calculate a single numeric value from document field values.

avg Aggregation

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "avg_response": {
      "avg": { "field": "response_time" }
    }
  }
}'
{
  "aggregations": {
    "avg_response": {
      "value": 968.6333312988281
    }
  }
}

sum Aggregation

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "total_response_time": {
      "sum": { "field": "response_time" }
    }
  }
}'
{
  "aggregations": {
    "total_response_time": {
      "value": 5811.799987792969
    }
  }
}

min and max Aggregations

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "fastest_response": {
      "min": { "field": "response_time" }
    },
    "slowest_response": {
      "max": { "field": "response_time" }
    }
  }
}'
{
  "aggregations": {
    "fastest_response": {
      "value": 15.100000381469727
    },
    "slowest_response": {
      "value": 5020.0
    }
  }
}

stats Aggregation

Returns count, min, max, avg, and sum in one call:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "response_stats": {
      "stats": { "field": "response_time" }
    }
  }
}'
{
  "aggregations": {
    "response_stats": {
      "count": 6,
      "min": 15.100000381469727,
      "max": 5020.0,
      "avg": 968.6333312988281,
      "sum": 5811.799987792969
    }
  }
}

cardinality Aggregation

Approximate count of distinct values (like SQL COUNT(DISTINCT ...)):

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "unique_methods": {
      "cardinality": { "field": "method" }
    },
    "unique_ips": {
      "cardinality": { "field": "client_ip" }
    }
  }
}'
{
  "aggregations": {
    "unique_methods": {
      "value": 3
    },
    "unique_ips": {
      "value": 3
    }
  }
}

Bucket Aggregations

Bucket aggregations group documents into buckets. Each bucket holds a set of documents that match a criterion.

terms Aggregation

Groups documents by distinct values of a field — like SQL GROUP BY:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "by_method": {
      "terms": { "field": "method" }
    }
  }
}'
{
  "aggregations": {
    "by_method": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        { "key": "GET", "doc_count": 4 },
        { "key": "DELETE", "doc_count": 1 },
        { "key": "POST", "doc_count": 1 }
      ]
    }
  }
}

Control the number of buckets with size:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "top_2_methods": {
      "terms": {
        "field": "method",
        "size": 2
      }
    }
  }
}'
{
  "aggregations": {
    "top_2_methods": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 1,
      "buckets": [
        { "key": "GET", "doc_count": 4 },
        { "key": "DELETE", "doc_count": 1 }
      ]
    }
  }
}

date_histogram Aggregation

Groups documents into time-based buckets — essential for time-series dashboards:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "requests_over_time": {
      "date_histogram": {
        "field": "@timestamp",
        "calendar_interval": "hour"
      }
    }
  }
}'
{
  "aggregations": {
    "requests_over_time": {
      "buckets": [
        {
          "key_as_string": "2026-04-01T08:00:00.000Z",
          "key": 1743494400000,
          "doc_count": 5
        },
        {
          "key_as_string": "2026-04-01T09:00:00.000Z",
          "key": 1743498000000,
          "doc_count": 1
        }
      ]
    }
  }
}

Use fixed_interval for exact durations:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "requests_per_10_min": {
      "date_histogram": {
        "field": "@timestamp",
        "fixed_interval": "10m"
      }
    }
  }
}'

range Aggregation

Groups documents into custom numeric ranges:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "response_time_ranges": {
      "range": {
        "field": "response_time",
        "ranges": [
          { "key": "fast",   "to": 100 },
          { "key": "normal", "from": 100, "to": 500 },
          { "key": "slow",   "from": 500 }
        ]
      }
    }
  }
}'
{
  "aggregations": {
    "response_time_ranges": {
      "buckets": [
        { "key": "fast", "to": 100.0, "doc_count": 2 },
        { "key": "normal", "from": 100.0, "to": 500.0, "doc_count": 3 },
        { "key": "slow", "from": 500.0, "doc_count": 1 }
      ]
    }
  }
}

filters Aggregation

Creates named buckets based on query filters:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "status_groups": {
      "filters": {
        "filters": {
          "success": { "range": { "status_code": { "gte": 200, "lt": 300 } } },
          "client_error": { "range": { "status_code": { "gte": 400, "lt": 500 } } },
          "server_error": { "range": { "status_code": { "gte": 500 } } }
        }
      }
    }
  }
}'
{
  "aggregations": {
    "status_groups": {
      "buckets": {
        "success": { "doc_count": 4 },
        "client_error": { "doc_count": 1 },
        "server_error": { "doc_count": 1 }
      }
    }
  }
}

histogram Aggregation

Groups by fixed-width numeric intervals:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "response_histogram": {
      "histogram": {
        "field": "response_time",
        "interval": 100,
        "min_doc_count": 1
      }
    }
  }
}'
{
  "aggregations": {
    "response_histogram": {
      "buckets": [
        { "key": 0.0, "doc_count": 1 },
        { "key": 100.0, "doc_count": 2 },
        { "key": 200.0, "doc_count": 1 },
        { "key": 300.0, "doc_count": 1 },
        { "key": 5000.0, "doc_count": 1 }
      ]
    }
  }
}

Nested / Sub-Aggregations

Bucket aggregations can contain sub-aggregations — metrics computed per bucket. This is where aggregations become truly powerful.

Metrics Inside Buckets

Average response time per HTTP method:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "by_method": {
      "terms": { "field": "method" },
      "aggs": {
        "avg_response": {
          "avg": { "field": "response_time" }
        },
        "max_response": {
          "max": { "field": "response_time" }
        }
      }
    }
  }
}'
{
  "aggregations": {
    "by_method": {
      "buckets": [
        {
          "key": "GET",
          "doc_count": 4,
          "avg_response": { "value": 110.39999961853027 },
          "max_response": { "value": 210.69999694824219 }
        },
        {
          "key": "DELETE",
          "doc_count": 1,
          "avg_response": { "value": 5020.0 },
          "max_response": { "value": 5020.0 }
        },
        {
          "key": "POST",
          "doc_count": 1,
          "avg_response": { "value": 350.20001220703125 },
          "max_response": { "value": 350.20001220703125 }
        }
      ]
    }
  }
}

Multi-Level Nesting

Group by method, then by status code range, with stats at each level:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "by_method": {
      "terms": { "field": "method" },
      "aggs": {
        "by_status_group": {
          "range": {
            "field": "status_code",
            "ranges": [
              { "key": "2xx", "from": 200, "to": 300 },
              { "key": "4xx", "from": 400, "to": 500 },
              { "key": "5xx", "from": 500, "to": 600 }
            ]
          },
          "aggs": {
            "response_stats": {
              "stats": { "field": "response_time" }
            }
          }
        }
      }
    }
  }
}'
{
  "aggregations": {
    "by_method": {
      "buckets": [
        {
          "key": "GET",
          "doc_count": 4,
          "by_status_group": {
            "buckets": [
              {
                "key": "2xx",
                "doc_count": 3,
                "response_stats": { "count": 3, "min": 95.3, "max": 210.7, "avg": 142.17, "sum": 426.5 }
              },
              {
                "key": "4xx",
                "doc_count": 1,
                "response_stats": { "count": 1, "min": 15.1, "max": 15.1, "avg": 15.1, "sum": 15.1 }
              },
              {
                "key": "5xx",
                "doc_count": 0,
                "response_stats": { "count": 0, "min": null, "max": null, "avg": null, "sum": 0.0 }
              }
            ]
          }
        },
        {
          "key": "DELETE",
          "doc_count": 1,
          "by_status_group": {
            "buckets": [
              { "key": "2xx", "doc_count": 0, "response_stats": { "count": 0 } },
              { "key": "4xx", "doc_count": 0, "response_stats": { "count": 0 } },
              {
                "key": "5xx",
                "doc_count": 1,
                "response_stats": { "count": 1, "min": 5020.0, "max": 5020.0, "avg": 5020.0, "sum": 5020.0 }
              }
            ]
          }
        }
      ]
    }
  }
}

Pipeline Aggregations

Pipeline aggregations work on the output of other aggregations rather than document fields.

cumulative_sum

Running total of request counts over time:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "requests_over_time": {
      "date_histogram": {
        "field": "@timestamp",
        "fixed_interval": "10m"
      },
      "aggs": {
        "request_count": {
          "value_count": { "field": "method" }
        },
        "cumulative_requests": {
          "cumulative_sum": {
            "buckets_path": "request_count"
          }
        }
      }
    }
  }
}'
{
  "aggregations": {
    "requests_over_time": {
      "buckets": [
        { "key_as_string": "2026-04-01T08:00:00.000Z", "doc_count": 2, "request_count": { "value": 2 }, "cumulative_requests": { "value": 2.0 } },
        { "key_as_string": "2026-04-01T08:10:00.000Z", "doc_count": 2, "request_count": { "value": 2 }, "cumulative_requests": { "value": 4.0 } },
        { "key_as_string": "2026-04-01T08:20:00.000Z", "doc_count": 1, "request_count": { "value": 1 }, "cumulative_requests": { "value": 5.0 } },
        { "key_as_string": "2026-04-01T09:00:00.000Z", "doc_count": 1, "request_count": { "value": 1 }, "cumulative_requests": { "value": 6.0 } }
      ]
    }
  }
}

derivative

Compute the rate of change between buckets:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "aggs": {
    "requests_over_time": {
      "date_histogram": {
        "field": "@timestamp",
        "fixed_interval": "10m"
      },
      "aggs": {
        "avg_response": {
          "avg": { "field": "response_time" }
        },
        "response_time_change": {
          "derivative": {
            "buckets_path": "avg_response"
          }
        }
      }
    }
  }
}'
{
  "aggregations": {
    "requests_over_time": {
      "buckets": [
        { "key_as_string": "2026-04-01T08:00:00.000Z", "avg_response": { "value": 235.35 } },
        { "key_as_string": "2026-04-01T08:10:00.000Z", "avg_response": { "value": 55.2 }, "response_time_change": { "value": -180.15 } },
        { "key_as_string": "2026-04-01T08:20:00.000Z", "avg_response": { "value": 5020.0 }, "response_time_change": { "value": 4964.8 } },
        { "key_as_string": "2026-04-01T09:00:00.000Z", "avg_response": { "value": 210.7 }, "response_time_change": { "value": -4809.3 } }
      ]
    }
  }
}

Combining Queries with Aggregations

Aggregations run on the documents matched by the query. This lets you scope your analytics:

curl -s -X GET "localhost:9200/web-logs/_search?pretty" -H 'Content-Type: application/json' -d '{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "term": { "method": "GET" } }
      ]
    }
  },
  "aggs": {
    "status_breakdown": {
      "terms": { "field": "status_code" }
    },
    "avg_response": {
      "avg": { "field": "response_time" }
    }
  }
}'
{
  "took": 2,
  "hits": { "total": { "value": 4, "relation": "eq" } },
  "aggregations": {
    "status_breakdown": {
      "buckets": [
        { "key": 200, "doc_count": 3 },
        { "key": 404, "doc_count": 1 }
      ]
    },
    "avg_response": {
      "value": 110.39999961853027
    }
  }
}

Only GET requests are included in the aggregations.

Quick Reference

Aggregation Type Purpose
avg Metric Average value
sum Metric Total sum
min / max Metric Minimum / maximum value
stats Metric All basic stats in one call
cardinality Metric Approximate distinct count
value_count Metric Count of values
terms Bucket Group by distinct values
date_histogram Bucket Group by time intervals
range Bucket Group by custom numeric ranges
filters Bucket Group by named query filters
histogram Bucket Group by fixed numeric intervals
cumulative_sum Pipeline Running total across buckets
derivative Pipeline Rate of change between buckets
size: 0 Pattern Skip hits, return only agg results

Next: Sorting and Pagination — Control result ordering, pagination strategies, and source filtering.