Consul’s prepared queries are not just for fetching service health; they’re actually a sophisticated, server-side routing mechanism that can automatically redirect traffic away from unhealthy instances before your application even knows there’s a problem.

Let’s watch a prepared query in action. Imagine we have a users-api service with three healthy instances: 10.0.1.10:8080, 10.0.1.11:8080, and 10.0.1.12:8080.

# First, let's query the healthy instances directly using the default prepared query
consul query -service users-api

# Expected output (or similar):
# Node        Address      Service Address  Service Port  Tags
# 10.0.1.10   10.0.1.10    10.0.1.10        8080
# 10.0.1.11   10.0.1.11    10.0.1.11        8080
# 10.0.1.12   10.0.1.12    10.0.1.12        8080

Now, let’s simulate a failure on 10.0.1.11. In a real scenario, Consul’s health checks would detect this.

# Imagine the health check for 10.0.1.11:8080 fails.
# Consul's internal state updates.

If we now run the same query again, Consul’s prepared query engine, which has access to the latest health information, will automatically exclude the failed instance.

# Querying again after a simulated failure
consul query -service users-api

# Expected output (notice 10.0.1.11 is gone):
# Node        Address      Service Address  Service Port  Tags
# 10.0.1.10   10.0.1.10    10.0.1.10        8080
# 10.0.1.12   10.0.1.12    10.0.1.12        8080

This is the core idea: the prepared query acts as a dynamic DNS or load balancer endpoint. Instead of your application hardcoding IPs or relying on a separate load balancer, it queries Consul. Consul, in turn, returns only healthy instances based on its registered health checks.

The problem prepared queries solve is the disconnect between service discovery and actual service availability. Traditional service discovery tells you what services exist and where they were registered. Prepared queries add the crucial layer of current health. They abstract away the complexity of managing health checks and dynamically updating endpoints.

Internally, a prepared query is a saved DNS query that Consul’s DNS interface (or its HTTP API) can execute on demand. When you query users-api.service.consul, Consul doesn’t just look up registered services; it runs a pre-defined query. This pre-defined query includes filtering logic for health status. You define this query via the Consul API or CLI, specifying the service name, the query type (e.g., service), and any filtering like passing health checks.

Here’s how you’d define such a prepared query using the CLI:

# Define a prepared query named "find-healthy-users-api"
# that looks for the "users-api" service and only returns passing instances.
consul query register -name="find-healthy-users-api" -service="users-api" -type="service" -filter="node()=~'.*' and service('users-api') and service.health='passing'"

# This creates a query you can then execute via the API or DNS.
# The DNS name for this query would be: find-healthy-users-api.query.consul

The filter="node()=~'.*' and service('users-api') and service.health='passing'" is the magic. It tells Consul to:

  • node()=~'.*': Match any node.
  • service('users-api'): Filter for services named users-api.
  • service.health='passing': Crucially, only include instances where the associated health checks are currently passing.

This allows applications to simply query find-healthy-users-api.query.consul (or use the HTTP API endpoint for the prepared query). Consul does the heavy lifting of checking the health status and returning only valid endpoints.

The most surprising thing about prepared queries is that they can be used to route traffic not just to healthy instances of a single service, but to the healthiest instance across multiple, potentially different, services based on arbitrary criteria. You can write a prepared query that asks, "Give me the instance of service-a that has the lowest latency and is healthy, or if that’s not available, give me the healthiest instance of service-b." This is achieved by chaining multiple service or node filters with or and and logic within the query definition, and by leveraging Consul’s built-in service.latency and service.votes sorting capabilities. This allows for sophisticated active-active or active-passive failover strategies defined entirely within Consul, without needing external load balancers or complex application-level logic.

After successfully implementing and using prepared queries for automatic service failover, the next challenge you’ll likely encounter is how to manage and propagate configuration changes for these queries across a large, distributed Consul cluster without manual intervention.

Want structured learning?

Take the full Consul course →