--- a/checker/consul_leader_checker.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package checker
-
-import (
-	"cmp"
-	"context"
-	"fmt"
-	"net/url"
-	"time"
-
-	"github.com/cybertec-postgresql/vip-manager/vipconfig"
-	"github.com/hashicorp/consul/api"
-)
-
-// ConsulLeaderChecker is used to check state of the leader key in Consul
-type ConsulLeaderChecker struct {
-	*vipconfig.Config
-	*api.Client
-}
-
-// NewConsulLeaderChecker returns a new instance
-func NewConsulLeaderChecker(con *vipconfig.Config) (lc *ConsulLeaderChecker, err error) {
-	lc = &ConsulLeaderChecker{Config: con}
-
-	url, err := url.Parse(con.Endpoints[0])
-	if err != nil {
-		return nil, fmt.Errorf("failed to parse consul endpoint URL %s: %w", con.Endpoints[0], err)
-	}
-
-	if url.Hostname() == "" {
-		return nil, fmt.Errorf("invalid consul endpoint URL: hostname is empty in %s", con.Endpoints[0])
-	}
-
-	config := &api.Config{
-		Address:  fmt.Sprintf("%s:%s", url.Hostname(), url.Port()),
-		Scheme:   url.Scheme,
-		WaitTime: time.Second,
-		Token:    cmp.Or(con.ConsulToken, ""),
-	}
-
-	if lc.Client, err = api.NewClient(config); err != nil {
-		return nil, fmt.Errorf("failed to create consul client for endpoint %s: %w", con.Endpoints[0], err)
-	}
-
-	return lc, nil
-}
-
-// GetChangeNotificationStream checks the status in the loop
-func (c *ConsulLeaderChecker) GetChangeNotificationStream(ctx context.Context, out chan<- bool) error {
-	kv := c.KV()
-
-	queryOptions := &api.QueryOptions{
-		RequireConsistent: true,
-	}
-
-checkLoop:
-	for {
-		resp, _, err := kv.Get(c.TriggerKey, queryOptions)
-		if err != nil {
-			if ctx.Err() != nil {
-				break checkLoop
-			}
-			c.Logger.Sugar().Error("consul error: ", err)
-			// Signal false on connection error so VIP is removed if endpoint is unreachable
-			// Guard the send with ctx to avoid deadlock during shutdown
-			select {
-			case out <- false:
-			case <-ctx.Done():
-				break checkLoop
-			}
-			time.Sleep(time.Duration(c.Interval) * time.Millisecond)
-			continue
-		}
-		if resp == nil {
-			c.Logger.Sugar().Errorf("Cannot get variable for key %s. Will try again in a second.", c.TriggerKey)
-			select {
-			case out <- false:
-			case <-ctx.Done():
-				break checkLoop
-			}
-			time.Sleep(time.Duration(c.Interval) * time.Millisecond)
-			continue
-		}
-
-		state := string(resp.Value) == c.TriggerValue
-		queryOptions.WaitIndex = resp.ModifyIndex
-
-		select {
-		case <-ctx.Done():
-			break checkLoop
-		case out <- state:
-			time.Sleep(time.Duration(c.Interval) * time.Millisecond)
-			continue
-		}
-	}
-
-	return ctx.Err()
-}
--- a/checker/leader_checker.go
+++ b/checker/leader_checker.go
@@ -21,8 +21,6 @@ func NewLeaderChecker(con *vipconfig.Con
 	var err error
 
 	switch con.EndpointType {
-	case "consul":
-		lc, err = NewConsulLeaderChecker(con)
 	case "etcd", "etcd3":
 		lc, err = NewEtcdLeaderChecker(con)
 	case "patroni":
--- a/checker/leader_checker_test.go
+++ b/checker/leader_checker_test.go
@@ -32,17 +32,6 @@ func TestNewLeaderChecker_Empty(t *testi
 	}
 }
 
-func TestNewLeaderChecker_Consul(t *testing.T) {
-	t.Parallel()
-	lc, err := NewLeaderChecker(newLeaderCheckerConfig("consul", []string{"http://127.0.0.1:8500"}))
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
-	}
-	if _, ok := lc.(*ConsulLeaderChecker); !ok {
-		t.Errorf("expected *ConsulLeaderChecker, got %T", lc)
-	}
-}
-
 func TestNewLeaderChecker_Etcd(t *testing.T) {
 	t.Parallel()
 	lc, err := NewLeaderChecker(newLeaderCheckerConfig("etcd", []string{"http://127.0.0.1:2379"}))
--- a/checker/consul_leader_checker_test.go
+++ /dev/null
@@ -1,281 +0,0 @@
-package checker
-
-import (
-	"context"
-	"errors"
-	"strings"
-	"testing"
-	"time"
-
-	"github.com/cybertec-postgresql/vip-manager/vipconfig"
-	capi "github.com/hashicorp/consul/api"
-	"github.com/testcontainers/testcontainers-go"
-	tcconsul "github.com/testcontainers/testcontainers-go/modules/consul"
-	"go.uber.org/zap"
-)
-
-func newTestConfig(endpoint string) *vipconfig.Config {
-	return &vipconfig.Config{
-		Endpoints: []string{endpoint},
-		Logger:    zap.NewNop(),
-	}
-}
-
-// TestNewConsulLeaderChecker_UnparseableURL verifies that a URL containing a
-// null byte (rejected by net/url) is wrapped with context.
-func TestNewConsulLeaderChecker_UnparseableURL(t *testing.T) {
-	t.Parallel()
-	_, err := NewConsulLeaderChecker(newTestConfig("http://invalid\x00host"))
-	if err == nil {
-		t.Fatal("expected error, got nil")
-	}
-	if !strings.Contains(err.Error(), "failed to parse consul endpoint URL") {
-		t.Errorf("unexpected error message: %v", err)
-	}
-}
-
-// TestNewConsulLeaderChecker_EmptyHostname verifies that a URL with no host
-// component (e.g. a bare path) is rejected with the empty-hostname sentinel.
-func TestNewConsulLeaderChecker_EmptyHostname(t *testing.T) {
-	t.Parallel()
-	// "localhost" without a scheme parses successfully but Hostname() == ""
-	_, err := NewConsulLeaderChecker(newTestConfig("localhost"))
-	if err == nil {
-		t.Fatal("expected error, got nil")
-	}
-	if !strings.Contains(err.Error(), "hostname is empty") {
-		t.Errorf("unexpected error message: %v", err)
-	}
-}
-
-// TestNewConsulLeaderChecker_ValidURL verifies that a well-formed endpoint
-// does not produce a construction error (api.NewClient never fails for valid
-// address strings).
-func TestNewConsulLeaderChecker_ValidURL(t *testing.T) {
-	t.Parallel()
-	lc, err := NewConsulLeaderChecker(newTestConfig("http://127.0.0.1:8500"))
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
-	}
-	if lc == nil {
-		t.Fatal("expected non-nil checker")
-	}
-}
-
-// ---------------------------------------------------------------------------
-// Integration tests – require a running Docker daemon
-// ---------------------------------------------------------------------------
-
-const consulImage = "hashicorp/consul:1.15"
-const consulTestKey = "service/batman/leader"
-
-// startConsulContainer starts a real Consul container and returns the HTTP API
-// endpoint (with scheme) and a seed client for writing test data.
-// The test is skipped when Docker is not available.
-func startConsulContainer(t *testing.T) (endpoint string, seed *capi.Client) {
-	t.Helper()
-	ctx := context.Background()
-	ctr, err := tcconsul.Run(ctx, consulImage)
-	if err != nil {
-		t.Skipf("cannot start consul container (Docker may be unavailable): %v", err)
-	}
-	testcontainers.CleanupContainer(t, ctr)
-
-	host, err := ctr.ApiEndpoint(ctx)
-	if err != nil {
-		t.Fatalf("ApiEndpoint: %v", err)
-	}
-
-	cfg := capi.DefaultConfig()
-	cfg.Address = host
-	seed, err = capi.NewClient(cfg)
-	if err != nil {
-		t.Fatalf("seed api.NewClient: %v", err)
-	}
-	return "http://" + host, seed
-}
-
-// consulCheckerFor builds a ConsulLeaderChecker with a 1 ms poll interval for
-// fast test iteration.
-func consulCheckerFor(t *testing.T, endpoint, key, value string) *ConsulLeaderChecker {
-	t.Helper()
-	conf := &vipconfig.Config{
-		Endpoints:    []string{endpoint},
-		TriggerKey:   key,
-		TriggerValue: value,
-		Interval:     1,
-		Logger:       zap.NewNop(),
-	}
-	checker, err := NewConsulLeaderChecker(conf)
-	if err != nil {
-		t.Fatalf("NewConsulLeaderChecker: %v", err)
-	}
-	return checker
-}
-
-// runConsulStream starts GetChangeNotificationStream in a goroutine and
-// returns the output channel and a done channel carrying the final error.
-// The channel is unbuffered so that, once the test stops reading, `out <-
-// state` always blocks and ctx.Done() is the guaranteed winner in the
-// production select – preventing spurious extra long-poll cycles after cancel.
-func runConsulStream(ctx context.Context, c *ConsulLeaderChecker) (out chan bool, done chan error) {
-	out = make(chan bool) // unbuffered
-	done = make(chan error, 1)
-	go func() { done <- c.GetChangeNotificationStream(ctx, out) }()
-	return
-}
-
-// receiveOne reads one value from out within 3 s or fails the test.
-func receiveOne(t *testing.T, out <-chan bool) bool {
-	t.Helper()
-	select {
-	case v := <-out:
-		return v
-	case <-time.After(3 * time.Second):
-		t.Fatal("timed out waiting for stream value")
-		return false
-	}
-}
-
-// waitDone waits for the stream goroutine to exit within 5 s (must allow for
-// the consul WaitTime=1 s long-poll to expire).
-func waitDone(t *testing.T, done <-chan error) error {
-	t.Helper()
-	select {
-	case err := <-done:
-		return err
-	case <-time.After(5 * time.Second):
-		t.Fatal("timed out waiting for GetChangeNotificationStream to return")
-		return nil
-	}
-}
-
-// TestConsulLeaderChecker_GetChangeNotificationStream_KeyAbsent verifies that
-// the stream emits false when the watched key is not present in Consul.
-// After cancelling, a key is injected so the goroutine can advance past the
-// nil-response path (which has no inline ctx check) and exit via the select.
-func TestConsulLeaderChecker_GetChangeNotificationStream_KeyAbsent(t *testing.T) {
-	endpoint, seed := startConsulContainer(t)
-	checker := consulCheckerFor(t, endpoint, consulTestKey, "primary")
-
-	ctx, cancel := context.WithCancel(context.Background())
-	out, done := runConsulStream(ctx, checker)
-
-	if receiveOne(t, out) {
-		t.Error("expected false for absent key, got true")
-	}
-
-	cancel()
-	// The nil-response branch has no inline ctx check and writes to `out`
-	// directly (not in a select). With an unbuffered channel the goroutine may
-	// therefore be stuck on that write. One extra read unblocks it so it can
-	// proceed. Injecting a key lets it advance to the resp-not-nil select where
-	// ctx.Done() fires cleanly.
-	_, _ = seed.KV().Put(&capi.KVPair{Key: consulTestKey, Value: []byte("any")}, nil)
-	select {
-	case <-out: // unblock the goroutine if stuck on a nil-response write
-	case <-time.After(time.Second): // goroutine already past that point – fine
-	}
-
-	if err := waitDone(t, done); !errors.Is(err, context.Canceled) {
-		t.Errorf("expected context.Canceled, got %v", err)
-	}
-}
-
-// TestConsulLeaderChecker_GetChangeNotificationStream_MatchingValue verifies
-// that the stream emits true when the key value equals TriggerValue.
-func TestConsulLeaderChecker_GetChangeNotificationStream_MatchingValue(t *testing.T) {
-	endpoint, seed := startConsulContainer(t)
-	if _, err := seed.KV().Put(&capi.KVPair{Key: consulTestKey, Value: []byte("primary")}, nil); err != nil {
-		t.Fatalf("seed Put: %v", err)
-	}
-	checker := consulCheckerFor(t, endpoint, consulTestKey, "primary")
-
-	ctx, cancel := context.WithCancel(context.Background())
-	out, done := runConsulStream(ctx, checker)
-
-	if !receiveOne(t, out) {
-		t.Error("expected true for matching value, got false")
-	}
-
-	cancel()
-	if err := waitDone(t, done); !errors.Is(err, context.Canceled) {
-		t.Errorf("expected context.Canceled, got %v", err)
-	}
-}
-
-// TestConsulLeaderChecker_GetChangeNotificationStream_NonMatchingValue verifies
-// that the stream emits false when the key value differs from TriggerValue.
-func TestConsulLeaderChecker_GetChangeNotificationStream_NonMatchingValue(t *testing.T) {
-	endpoint, seed := startConsulContainer(t)
-	if _, err := seed.KV().Put(&capi.KVPair{Key: consulTestKey, Value: []byte("secondary")}, nil); err != nil {
-		t.Fatalf("seed Put: %v", err)
-	}
-	checker := consulCheckerFor(t, endpoint, consulTestKey, "primary")
-
-	ctx, cancel := context.WithCancel(context.Background())
-	out, done := runConsulStream(ctx, checker)
-
-	if receiveOne(t, out) {
-		t.Error("expected false for non-matching value, got true")
-	}
-
-	cancel()
-	if err := waitDone(t, done); !errors.Is(err, context.Canceled) {
-		t.Errorf("expected context.Canceled, got %v", err)
-	}
-}
-
-// TestConsulLeaderChecker_GetChangeNotificationStream_KeyChanges verifies that
-// the stream picks up a value change via the blocking long-poll.
-func TestConsulLeaderChecker_GetChangeNotificationStream_KeyChanges(t *testing.T) {
-	endpoint, seed := startConsulContainer(t)
-	if _, err := seed.KV().Put(&capi.KVPair{Key: consulTestKey, Value: []byte("primary")}, nil); err != nil {
-		t.Fatalf("seed Put: %v", err)
-	}
-	checker := consulCheckerFor(t, endpoint, consulTestKey, "primary")
-
-	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
-	out, done := runConsulStream(ctx, checker)
-
-	// Initial value: matching → true.
-	if !receiveOne(t, out) {
-		t.Error("expected true for initial matching value, got false")
-	}
-
-	// Change the value while the stream is long-polling; the poll unblocks immediately.
-	if _, err := seed.KV().Put(&capi.KVPair{Key: consulTestKey, Value: []byte("secondary")}, nil); err != nil {
-		t.Fatalf("update Put: %v", err)
-	}
-
-	// Updated value: non-matching → false.
-	if receiveOne(t, out) {
-		t.Error("expected false after key change to non-matching value, got true")
-	}
-
-	cancel()
-	if err := waitDone(t, done); !errors.Is(err, context.Canceled) {
-		t.Errorf("expected context.Canceled, got %v", err)
-	}
-}
-
-// TestConsulLeaderChecker_GetChangeNotificationStream_ErrorPath verifies that
-// a KV error (unreachable server) causes the stream to emit false and that
-// cancelling the context stops it cleanly.
-func TestConsulLeaderChecker_GetChangeNotificationStream_ErrorPath(t *testing.T) {
-	// Port 1 is closed on loopback; the TCP dial fails immediately.
-	checker := consulCheckerFor(t, "http://127.0.0.1:1", consulTestKey, "primary")
-
-	ctx, cancel := context.WithCancel(context.Background())
-	out, done := runConsulStream(ctx, checker)
-
-	if receiveOne(t, out) {
-		t.Error("expected false on error path, got true")
-	}
-
-	cancel()
-	if err := waitDone(t, done); !errors.Is(err, context.Canceled) {
-		t.Errorf("expected context.Canceled, got %v", err)
-	}
-}
