Skip to content

gRPC Naming and Discovery — Service Resolution with DNS, Consul, and Kubernetes

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about grpc naming and discovery. We cover key concepts, practical examples, and best practices to help you master this topic.

gRPC naming and discovery resolves service names to IP addresses using DNS, Consul, Kubernetes, or custom resolvers, enabling clients to find and connect to available server instances dynamically.

What You'll Learn

  • DNS-based service resolution for gRPC
  • Kubernetes headless services for pod discovery
  • Consul integration for service registry
  • Building custom name resolvers
  • Health-aware service selection
  • Caching and re-resolution strategies

Why It Matters

In a dynamic environment like Kubernetes, service instances come and go. Clients need to discover available instances and distribute load across them without manual configuration. DodaTech's Durga Antivirus Pro uses DNS-based resolution with Kubernetes headless services, automatically discovering new pods within 30 seconds of deployment.

Real-World Use

A gRPC client connects to the threat service via DNS name threat-service.default.svc.cluster.local. When the service scales from 3 to 10 pods, the DNS record updates automatically. The client's DNS resolver discovers the new pods and distributes load across all 10 within the next resolution interval.

flowchart LR
    A["Client\ngrpc.Dial('threat-service')"] --> B["Name Resolver\n(dns://)"]
    B --> C["DNS Query\nSRV or A records"]
    C --> D["threat-service\n10.0.0.1:50051\n10.0.0.2:50051\n10.0.0.3:50051"]
    D --> E["Load Balancer\n(round_robin)"]
    E --> F["Selected Backend"]
    F --> G["gRPC Call"]
    style B fill:#fef3c7,stroke:#d97706
    style D fill:#dbeafe,stroke:#2563eb

Code Examples

Example 1: DNS-Based Resolution in Go

package main

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/resolver"
)

func init() {
    // Register DNS resolver scheme
    resolver.SetDefaultScheme("dns")
}

func main() {
    // DNS-based resolution with round-robin
    conn, _ := grpc.Dial(
        "dns:///threat-service.default.svc.cluster.local:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithDefaultServiceConfig(`{
            "loadBalancingConfig": [{"round_robin": {}}],
            "methodConfig": [{
                "name": [{"service": ""}],
                "retryPolicy": {
                    "maxAttempts": 3,
                    "initialBackoff": "0.1s",
                    "maxBackoff": "1s",
                    "backoffMultiplier": 2,
                    "retryableStatusCodes": ["UNAVAILABLE"]
                }
            }]
        }`),
        grpc.WithResolvers(&customResolver{}),
    )
    
    client := pb.NewThreatServiceClient(conn)
}

// Custom resolver builder
type customResolver struct{}

func (r *customResolver) Build(target resolver.Target,
    cc resolver.ClientConn,
    opts resolver.BuildOptions,
) (resolver.Resolver, error) {
    
    // Resolve addresses
    addrs := []resolver.Address{
        {Addr: "10.0.0.1:50051"},
        {Addr: "10.0.0.2:50051"},
    }
    
    cc.UpdateState(resolver.State{
        Addresses: addrs,
    })
    
    return &resolverState{cc: cc}, nil
}

func (r *customResolver) Scheme() string {
    return "custom"
}

type resolverState struct {
    cc resolver.ClientConn
}

func (r *resolverState) ResolveNow(o resolver.ResolveNowOptions) {
    // Trigger re-resolution
}

func (r *resolverState) Close() {}

Example 2: Consul Integration in Python

import grpc
import consul
import random

class ConsulResolver:
    def __init__(self, consul_host="localhost",
                 consul_port=8500):
        self.consul = consul.Consul(
            host=consul_host, port=consul_port)
        self.cache = {}
    
    def resolve(self, service_name):
        """Resolve service to available instances."""
        if service_name in self.cache:
            instances, index = self.cache[service_name]
            # Use blocking query for long polling
            instances, index = self.consul.health.service(
                service_name, index=index, wait="30s")
            self.cache[service_name] = (instances, index)
        else:
            instances, index = self.consul.health.service(
                service_name)
            self.cache[service_name] = (instances, index)
        
        # Filter healthy instances
        healthy = [
            i for i in instances
            if i["Checks"][0]["Status"] == "passing"
        ]
        
        if not healthy:
            raise Exception(
                f"No healthy instances for {service_name}")
        
        # Pick random instance (load balance)
        instance = random.choice(healthy)
        service = instance["Service"]
        address = service.get("Address")
        port = service["Port"]
        
        return f"{address}:{port}"
    
    def create_channel(self, service_name):
        address = self.resolve(service_name)
        return grpc.insecure_channel(address)

# Usage
resolver = ConsulResolver()
channel = resolver.create_channel("threat-service")
stub = pb.ThreatServiceStub(channel)

Example 3: Kubernetes-Aware Client in Node.js

const grpc = require('@grpc/grpc-js');
const { K8sResolver } = require('@grpc/resolver-k8s');

// Register Kubernetes resolver
grpc.resolver.register(K8sResolver);

// Kubernetes-aware connection
const client = new ThreatServiceClient(
  'k8s:///default/threat-service:50051',
  grpc.credentials.createInsecure(),
  {
    'grpc.lb_policy_name': 'round_robin',
    'grpc.dns_min_time_between_resolutions_ms': 5000,
  },
);

// Watch for endpoint changes
function watchEndpoints(namespace, service) {
  const watch = new K8sResolver.EndpointWatcher(
    namespace, service);
  
  watch.on('endpoints', (endpoints) => {
    console.log('Available endpoints:', endpoints);
  });
  
  watch.on('error', (error) => {
    console.error('Endpoint watch error:', error);
  });
  
  return watch;
}

// Custom health-aware resolver
class HealthAwareResolver {
  constructor() {
    this.healthyBackends = new Map();
  }
  
  addBackend(address) {
    this.healthyBackends.set(address, {
      healthy: true,
      failCount: 0,
    });
  }
  
  markUnhealthy(address) {
    const backend = this.healthyBackends.get(address);
    if (backend) {
      backend.failCount++;
      if (backend.failCount > 3) {
        backend.healthy = false;
        console.log(`Marked ${address} as unhealthy`);
        // Trigger re-resolution after 30s
        setTimeout(() => {
          backend.healthy = true;
          backend.failCount = 0;
        }, 30000);
      }
    }
  }
  
  getAddresses() {
    return Array.from(this.healthyBackends.entries())
      .filter(([_, status]) => status.healthy)
      .map(([addr]) => ({ addr }));
  }
}

Common Mistakes

  1. Using ClusterIP with kube-proxy — kube-proxy load balances at TCP level, breaking HTTP/2 multiplexing. Use headless services with client-side Load Balancing.
  2. Not setting DNS resolution intervals — default DNS caching (5 minutes) means new pods aren't discovered quickly. Set grpc.dns_min_time_between_resolutions_ms to 5-30s.
  3. Ignoring health in service selection — DNS may return unhealthy pods. Use readiness probes and remove failing instances from the active set.
  4. Using IP addresses instead of DNS — hardcoded IPs break when pods restart. Always use DNS names for gRPC connections.
  5. Not handling resolution failures — if DNS fails during initial resolution, the client can't connect. Implement fallback addresses and retry logic.

Practice Questions

  1. How does DNS-based resolution work in gRPC?
  2. Why do gRPC services need headless Kubernetes services?
  3. How does Consul's health checking improve service discovery?
  4. What is the role of a custom name resolver?
  5. How often should clients re-resolve DNS?

Challenge: Design a service discovery system for a gRPC microservice architecture that: uses DNS for initial resolution, Consul for health-aware selection, Kubernetes for endpoint watching, provides circuit breaker for unhealthy instances, and caches resolved addresses across restarts.

Mini Project

Build a gRPC service discovery library with: DNS resolver with configurable polling interval, Consul integration with health filtering, Kubernetes endpoint watcher, circuit breaker for failing instances, and load balancing strategies (round-robin, least-loaded, random).

FAQ

What is the default gRPC name resolver?

The default resolver uses DNS. The scheme is dns://. Without specifying, gRPC uses DNS resolution for the target address.

How do I use SRV records for gRPC?

gRPC doesn't natively support SRV records. Use DNS A records or a custom resolver that queries SRV records and maps to addresses.

Can I use environment variables for service discovery?

For development only. In production, use DNS, Consul, or Kubernetes. Environment variables don't update when instances change.

How does gRPC handle DNS resolution failures?

The resolver returns an error and the channel attempts to reconnect. Configure initial and max reconnect backoff for retry behavior.

What happens when all instances are unhealthy?

The resolver returns no addresses. The channel stays in TRANSIENT_FAILURE state. Clients should implement fallback or circuit breaker patterns.

What's Next

Learn about gRPC load balancing

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro