PromQL and HelixQL solve overlapping problems with different syntax. A direct mental mapping gets most dashboards translated in an afternoon, not a rewrite.
Teams migrating off Prometheus usually expect the hardest part to be the query language. It is not, once the mapping between the two clicks, the actual work is deciding which dashboards are still worth keeping rather than quietly retiring.
1. Map the core functions directly
Most PromQL rate and aggregation functions have a near one-to-one HelixQL equivalent, just written left to right instead of nested.
# PromQL
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, route))
# HelixQL
from metrics
| where metric.name == "http.request_duration"
| summarize p95(value) by route, bin(timestamp, 5m)2. Replace label matchers with where clauses
PromQL label selectors in curly braces become ordinary where clauses, with the same regex and negative-match support most PromQL dashboards rely on.
# PromQL
up{job="checkout-api", env=~"prod.*"}
# HelixQL
from metrics
| where metric.name == "up"
| where service.name == "checkout-api"
| where environment matches "prod.*"3. Do not migrate recording rules one-to-one
Prometheus recording rules exist largely to work around the query engine having to recompute expensive aggregations on every dashboard load. HelixQL's query planner caches and pushes down aggregations automatically, so most recording rules can simply become the equivalent live query rather than a separately maintained pre-computation.
The rules worth keeping as a separate scheduled job are the few that feed a long-window rollup, a thirty-day trailing average used in a capacity report, for instance, where recomputing from raw data on every load would be wasteful regardless of how good the planner is. Everything else is simpler as a live query maintained in one place instead of two.
4. Run both in parallel before cutting over
For any dashboard feeding an SLO or a paging alert, run the HelixQL version alongside the existing Prometheus one for at least a full week before switching the alert rule over. The two systems compute percentiles slightly differently at the edges, close enough that it rarely matters, different enough that a threshold tuned against one can occasionally behave differently against the other.
from metrics
| where metric.name == "http.request_duration"
| summarize p95_helix = p95(value), p95_prom_shadow = p95(value_from_shadow_export) by bin(timestamp, 5m)- Map rate and aggregation functions directly, the shape translates left to right.
- Replace label matchers with ordinary
whereclauses, including regex matches. - Drop most recording rules, HelixQL's planner handles the aggregation cost automatically.
- Run new and old dashboards in parallel for a week before cutting over any alert rule.