﻿---
title: Get started with PromQL in Elasticsearch
description: Ingest sample Prometheus metrics into Elasticsearch and query them with PromQL through the Prometheus-compatible HTTP API and ES|QL.
url: https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/promql-tutorial
products:
  - Elasticsearch
applies_to:
  - Elastic Cloud Serverless: Generally available
  - Elastic Stack: Generally available since 9.5, Preview in 9.4
---

# Get started with PromQL in Elasticsearch
Learn how to ingest sample Prometheus metrics into Elasticsearch and query them with [PromQL](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql). This tutorial walks you through ingesting a sample counter metric, running PromQL queries with the [Prometheus-compatible HTTP API](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/promql-http-api), post-processing PromQL results with ES|QL, and connecting Prometheus with remote write.
This tutorial assumes you have basic knowledge of:
- Prometheus metrics, samples, and labels
- REST APIs and JSON
- Basic PromQL selectors and functions

By the end of this tutorial, you can:
- Ingest Prometheus-style samples into a [time series index](https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/manage-data/data-store/data-streams/time-series-data-stream-tsds)
- Run instant PromQL queries with label matchers, aggregations, and `rate()`
- Run PromQL as an ES|QL source command and post-process the results with ES|QL
- Configure Prometheus remote write to ingest metrics continuously


## Before you begin

To get started with PromQL in Elasticsearch, you need the following:
- An Elasticsearch deployment with PromQL support
- Permissions to create an index and write and read documents in it
- Access to [Console](https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/explore-analyze/query-filter/tools/console) in Kibana, or a terminal with `curl`

<note>
  If you use `curl`, set the following environment variables to simplify the commands in this tutorial:
  ```bash
  export ELASTICSEARCH_URL="your-elasticsearch-url"
  export API_KEY="your-api-key"
  ```
</note>

This tutorial uses fixed sample timestamps on 2026-10-22 so that every query returns the same result whenever you run it. To accept those timestamps, it stores the samples in a dedicated time series index with explicit time bounds instead of the default Prometheus data stream. The index mapping is a reduced version of the built-in `metrics-prometheus@mappings` component template that remote write uses: it only maps `_total` metrics as counters and omits the gauge and other counter mappings, so don't reuse it for production data. Metrics that you ingest with [Prometheus remote write](#promql-tutorial-remote-write) use current timestamps and don't need this setup.
You can also get help from your coding agent to write PromQL queries:
<agent-skill url="https://github.com/elastic/agent-skills@elasticsearch-esql">
  A skill is available to help AI agents with this topic.
  Agent skill that assists with ES|QL query generation including the PROMQL command.
</agent-skill>


## Step 0: Add sample Prometheus metrics

This tutorial uses an example counter metric, `requests_total`, that tracks HTTP requests for two instances, `us-east-1` and `us-west-1`. Each instance has three samples, one minute apart. The `us-east-1` counter increases by 7 requests per minute, and the `us-west-1` counter increases by 3 requests per minute.
Create the `promql-tutorial` time series index:
<tab-set>
  <tab-item title="Console">
    ```json

    {
      "settings": {
        "index": {
          "mode": "time_series", <1>
          "routing_path": ["labels.*"], <2>
          "time_series": {
            "start_time": "2026-10-22T11:00:00Z", <3>
            "end_time": "2026-10-22T13:00:00Z"
          }
        }
      },
      "mappings": {
        "dynamic_templates": [
          {
            "counter": {
              "path_match": ["metrics.*_total"], <4>
              "mapping": { "type": "double", "time_series_metric": "counter" }
            }
          }
        ],
        "properties": {
          "@timestamp": { "type": "date" },
          "labels": { "type": "passthrough", "priority": 10, "time_series_dimension": true }, <5>
          "metrics": { "type": "passthrough", "priority": 20 } <6>
        }
      }
    }
    ```

    1. PromQL queries only run against time series indices and [time series data streams](https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/manage-data/data-store/data-streams/time-series-data-stream-tsds) (TSDS).
    2. Route documents by their label dimensions.
    3. Accept the fixed sample timestamps that this tutorial uses.
    4. Map metrics with names that end in `_total`, such as `requests_total`, as counters.
    5. Store each Prometheus label, including the `__name__` metric name label, as a time series dimension.
    6. Expose each field under `metrics` by its short name, so `metrics.requests_total` is queryable as `requests_total`.
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -X PUT "${ELASTICSEARCH_URL}/promql-tutorial" \
         -H "Authorization: ApiKey ${API_KEY}" \
         -H "Content-Type: application/json" \
         -d '{
           "settings": {
             "index": {
               "mode": "time_series",
               "routing_path": ["labels.*"],
               "time_series": {
                 "start_time": "2026-10-22T11:00:00Z",
                 "end_time": "2026-10-22T13:00:00Z"
               }
             }
           },
           "mappings": {
             "dynamic_templates": [
               {
                 "counter": {
                   "path_match": ["metrics.*_total"],
                   "mapping": { "type": "double", "time_series_metric": "counter" }
                 }
               }
             ],
             "properties": {
               "@timestamp": { "type": "date" },
               "labels": { "type": "passthrough", "priority": 10, "time_series_dimension": true },
               "metrics": { "type": "passthrough", "priority": 20 }
             }
           }
         }'
    ```
  </tab-item>
</tab-set>

Add the sample metrics with the bulk API:
<tab-set>
  <tab-item title="Console">
    ```json

    { "create": {} }
    {"@timestamp":"2026-10-22T12:05:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-east-1"},"metrics":{"requests_total":170}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:05:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-west-1"},"metrics":{"requests_total":80}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:06:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-east-1"},"metrics":{"requests_total":177}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:04:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-east-1"},"metrics":{"requests_total":163}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:04:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-west-1"},"metrics":{"requests_total":77}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:06:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-west-1"},"metrics":{"requests_total":83}}
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -X POST "${ELASTICSEARCH_URL}/promql-tutorial/_bulk?refresh" \
         -H "Authorization: ApiKey ${API_KEY}" \
         -H "Content-Type: application/x-ndjson" \
         --data-binary @- << 'EOF'
    { "create": {} }
    {"@timestamp":"2026-10-22T12:05:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-east-1"},"metrics":{"requests_total":170}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:05:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-west-1"},"metrics":{"requests_total":80}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:06:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-east-1"},"metrics":{"requests_total":177}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:04:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-east-1"},"metrics":{"requests_total":163}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:04:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-west-1"},"metrics":{"requests_total":77}}
    { "create": {} }
    {"@timestamp":"2026-10-22T12:06:49Z","labels":{"__name__":"requests_total","job":"tutorial","instance":"us-west-1"},"metrics":{"requests_total":83}}
    EOF
    ```
  </tab-item>
</tab-set>

**Result:** The response confirms that all six documents were created, with `"errors": false` and a `created` result for each item.
<dropdown title="Example response (truncated)">
  ```json
  {
    "errors": false,
    "took": 12,
    "items": [
      {
        "create": {
          "_index": "promql-tutorial",
          "_id": "LDL-KSuugm8OtHL48HlRdj02ELXbaZuh8sbT1HvAVjf7WZWwiRB_MV...",
          "_version": 1,
          "result": "created",
          "status": 201
        }
      }
    ]
  }
  ```
</dropdown>

In practice, you ingest Prometheus metrics continuously with remote write or another collection pipeline instead of the bulk API. Refer to [Step 3: Connect Prometheus with remote write](#promql-tutorial-remote-write).

## Step 1: Run basic PromQL queries with the HTTP API

Elasticsearch exposes a [Prometheus-compatible HTTP API](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/promql-http-api) for running PromQL against stored metrics. The queries in this step use the [instant query](/elastic/elasticsearch/tree/main/reference/query-languages/promql/promql-http-api#promql-http-api-query-instant) endpoint, scoped to the `promql-tutorial` index with the `/_prometheus/{index}/api/v1/query` path form. Each query sets `time=1792670839`, which is 2026-10-22T12:07:19Z, thirty seconds after the last sample. If you omit `time`, the query runs at the current time.

### Run an instant query

Retrieve the latest value of `requests_total` for each instance:
<tab-set>
  <tab-item title="Console">
    ```json
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -G "${ELASTICSEARCH_URL}/_prometheus/promql-tutorial/api/v1/query" \
         -H "Authorization: ApiKey ${API_KEY}" \
         --data-urlencode "query=requests_total" \
         --data-urlencode "time=1792670839"
    ```
  </tab-item>
</tab-set>

**Result:** The response has a `resultType` of `vector` with two series, one per instance. Each series includes its labels and its latest counter value: `177.0` for `us-east-1` and `83.0` for `us-west-1`.
<dropdown title="Example response">
  ```json
  {
    "status": "success",
    "data": {
      "resultType": "vector",
      "result": [
        {
          "metric": {
            "__name__": "requests_total",
            "instance": "us-east-1",
            "job": "tutorial"
          },
          "value": [1.792670839E9, "177.0"]
        },
        {
          "metric": {
            "__name__": "requests_total",
            "instance": "us-west-1",
            "job": "tutorial"
          },
          "value": [1.792670839E9, "83.0"]
        }
      ]
    }
  }
  ```
</dropdown>


### Filter with label matchers

Use label matchers to select specific series. The following query only returns the `us-east-1` series:
<tab-set>
  <tab-item title="Console">
    ```json
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -G "${ELASTICSEARCH_URL}/_prometheus/promql-tutorial/api/v1/query" \
         -H "Authorization: ApiKey ${API_KEY}" \
         --data-urlencode 'query=requests_total{instance="us-east-1"}' \
         --data-urlencode "time=1792670839"
    ```
  </tab-item>
</tab-set>

**Result:** The response contains one series, with `instance="us-east-1"` and a value of `177.0`.

### Aggregate across series

Aggregate values across instances with `sum()`. The `by(job)` clause groups the result by the `job` label:
<tab-set>
  <tab-item title="Console">
    ```json
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -G "${ELASTICSEARCH_URL}/_prometheus/promql-tutorial/api/v1/query" \
         -H "Authorization: ApiKey ${API_KEY}" \
         --data-urlencode "query=sum by(job)(requests_total)" \
         --data-urlencode "time=1792670839"
    ```
  </tab-item>
</tab-set>

**Result:** The response contains one series, with `job="tutorial"` and a value of `260.0`, the sum of both instance counters.

### Select the largest series with `topk()`

<applies-to>Elastic Stack: Generally available since 9.5</applies-to> <applies-to>Elastic Cloud Serverless: Generally available</applies-to>
Use `topk()` to select the series with the largest values. The following query returns the instance with the highest request count:
<tab-set>
  <tab-item title="Console">
    ```json
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -G "${ELASTICSEARCH_URL}/_prometheus/promql-tutorial/api/v1/query" \
         -H "Authorization: ApiKey ${API_KEY}" \
         --data-urlencode "query=topk(1,requests_total)" \
         --data-urlencode "time=1792670839"
    ```
  </tab-item>
</tab-set>

**Result:** The response contains one series, with `instance="us-east-1"` and a value of `177.0`.

### Calculate a per-second rate with `rate()`

Use `rate()` to calculate the per-second increase of a counter over a time window. The following query sums the request rate over the last five minutes across both instances:
<tab-set>
  <tab-item title="Console">
    ```json
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -G "${ELASTICSEARCH_URL}/_prometheus/promql-tutorial/api/v1/query" \
         -H "Authorization: ApiKey ${API_KEY}" \
         --data-urlencode "query=sum(rate(requests_total[5m]))" \
         --data-urlencode "time=1792670839"
    ```
  </tab-item>
</tab-set>

**Result:** The response contains one series with an empty label set and a value of `0.1`, the combined request rate in requests per second.
<dropdown title="Example response">
  ```json
  {
    "status": "success",
    "data": {
      "resultType": "vector",
      "result": [
        {
          "metric": {},
          "value": [1.792670839E9, "0.1"]
        }
      ]
    }
  }
  ```
</dropdown>


## Step 2: Process PromQL results with ES|QL

You can also run PromQL as an ES|QL source command. Unlike the HTTP API, which returns Prometheus-compatible JSON, the [`PROMQL` command](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/esql/commands/promql) returns tabular results that you can process with standard ES|QL commands.
The following query calculates the combined request rate every minute for a twelve-minute range that ends thirty seconds after the last sample. The `start` and `end` options set the range, `step` sets the resolution, and `http_rate` names the result column:
<tab-set>
  <tab-item title="Console">
    ```json

    {
      "query": """
        PROMQL index=promql-tutorial start=1792670119 end=1792670839 step=60s
          http_rate=(sum(rate(requests_total[5m])))
        | SORT step DESC
        """
    }
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -X POST "${ELASTICSEARCH_URL}/_query?format=txt" \
         -H "Authorization: ApiKey ${API_KEY}" \
         -H "Content-Type: application/json" \
         -d '{
           "query": "PROMQL index=promql-tutorial start=1792670119 end=1792670839 step=60s http_rate=(sum(rate(requests_total[5m]))) | SORT step DESC"
         }'
    ```
  </tab-item>
</tab-set>

**Result:** The response is a table with `http_rate` and `step` columns. It contains one row for each minute in which a rate can be calculated from the samples.
<dropdown title="Example response">
  ```text
       http_rate     |          step
  -------------------+------------------------
  0.1                |2026-10-22T12:07:19.000Z
  0.06666666666666667|2026-10-22T12:06:19.000Z
  ```
</dropdown>

Extend the same query with ES|QL to filter out low rates and calculate the average and peak rate:
<tab-set>
  <tab-item title="Console">
    ```json

    {
      "query": """
        PROMQL index=promql-tutorial start=1792670119 end=1792670839 step=60s
          http_rate=(sum(rate(requests_total[5m])))
        | WHERE http_rate > 0.07
        | STATS avg_rate = AVG(http_rate), peak_rate = MAX(http_rate)
      """
    }
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -X POST "${ELASTICSEARCH_URL}/_query" \
         -H "Authorization: ApiKey ${API_KEY}" \
         -H "Content-Type: application/json" \
         -d '{
           "query": "PROMQL index=promql-tutorial start=1792670119 end=1792670839 step=60s http_rate=(sum(rate(requests_total[5m]))) | WHERE http_rate > 0.07 | STATS avg_rate = AVG(http_rate), peak_rate = MAX(http_rate)"
         }'
    ```
  </tab-item>
</tab-set>

**Result:** The response contains one row with `avg_rate` and `peak_rate` columns. For the sample data, the filter drops the `0.0667` row at 12:06:19, so both the average and the peak rate are `0.1` requests per second.
<dropdown title="Example response (truncated)">
  ```json
  {
    "columns": [
      { "name": "avg_rate", "type": "double" },
      { "name": "peak_rate", "type": "double" }
    ],
    "values": [
      [0.1, 0.1]
    ]
  }
  ```
</dropdown>

<tip>
  In Kibana, you can omit `start` and `end` so that the query follows the date picker, and omit range selectors such as `[5m]` so that the window adapts to the time range and step. Refer to [`PROMQL` command](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/esql/commands/promql) for the full syntax and best practices.
</tip>


## Step 3: Connect Prometheus with remote write

To ingest metrics continuously, configure Prometheus to send samples directly to Elasticsearch with remote write. Add the following configuration to your `prometheus.yml` file, replacing `<es-endpoint>` and `<api-key>` with your endpoint and credentials. The API key requires write access to the target data stream.
```yaml
remote_write:
  - url: "https://<es-endpoint>/_prometheus/api/v1/write"
    authorization:
      type: ApiKey
      credentials: "<api-key>"
```

By default, metrics are stored in the `metrics-generic.prometheus-default` data stream. To write to a custom dataset, include it in the endpoint URL:
```yaml
remote_write:
  - url: "https://<es-endpoint>/_prometheus/metrics/tutorial/api/v1/write"
    authorization:
      type: ApiKey
      credentials: "<api-key>"
```

This configuration writes to `metrics-tutorial.prometheus-default`. The query shapes from the previous steps work against remote-write metrics with your own metric names in place of `requests_total`, and without an index segment in the URL, because the HTTP API queries `metrics-*` by default. For example, use `/_prometheus/api/v1/query` instead of `/_prometheus/promql-tutorial/api/v1/query`, and omit `time` to query at the current time. For more information, refer to [Ingest data with Prometheus remote write](https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/manage-data/data-store/data-streams/tsds-ingest-prometheus-remote-write).

## Clean up resources (optional)

Delete the tutorial index and its sample data:
<tab-set>
  <tab-item title="Console">
    ```json
    ```
  </tab-item>

  <tab-item title="curl">
    ```bash
    curl -X DELETE "${ELASTICSEARCH_URL}/promql-tutorial" \
         -H "Authorization: ApiKey ${API_KEY}"
    ```
  </tab-item>
</tab-set>


## Summary

In this tutorial, you learned how to:
- Ingest Prometheus-style samples into a time series index
- Run instant PromQL queries, label matcher filters, aggregations, and rate calculations with the Prometheus-compatible HTTP API
- Run PromQL with the ES|QL `PROMQL` command and post-process the results with `WHERE` and `STATS`
- Configure Prometheus remote write for continuous ingestion


## Next steps

- Connect Grafana and run PromQL from dashboards. Refer to [Prometheus data source in Grafana](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/promql-grafana).
- Explore the supported PromQL [functions](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/functions) and [operators](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/operators), including differences from Prometheus.
- Review the behavioral differences from Prometheus before you port production queries. Refer to [Limitations](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/promql-limitations).


## Related pages

- [PromQL reference](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql)
- [PromQL HTTP API](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/promql/promql-http-api)
- [ES|QL `PROMQL` command](https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/reference/query-languages/esql/commands/promql)
- [Ingest data with Prometheus remote write](https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/manage-data/data-store/data-streams/tsds-ingest-prometheus-remote-write)
- [Time series data streams](https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/manage-data/data-store/data-streams/time-series-data-stream-tsds)