testcontainers-go-redis-cluster
A Go library for creating Redis clusters in tests using testcontainers-go. Provides two implementations: Redis (V1) and RedisV2 (recommended, faster).
Quick Start
package main
import (
"context"
"testing"
"github.com/NSXBet/testcontainers-go-redis-cluster"
"github.com/redis/go-redis/v9"
)
func TestMyFeature(t *testing.T) {
// RECOMMENDED: Use RedisV3 for fast, reliable cluster testing (~1s startup, 100% reliable)
connStr := tcredis.RedisV3(t) // Single-node emulated cluster (Dragonfly)
// Alternative: Use RedisV2 for multi-node cluster testing (~8s startup)
// Note: RedisV2 has ~50% reliability due to Redis gossip protocol issues in Docker
// connStr := tcredis.RedisV2(t, 3) // 3-node cluster
// Parse connection string and create cluster client
opts, err := redis.ParseClusterURL(connStr)
if err != nil {
t.Fatalf("Failed to parse connection string: %v", err)
}
client := redis.NewClusterClient(opts)
defer client.Close()
// Use the cluster client
ctx := context.Background()
err = client.Set(ctx, "key", "value", 0).Err()
if err != nil {
t.Fatalf("Failed to set key: %v", err)
}
val, err := client.Get(ctx, "key").Result()
if err != nil {
t.Fatalf("Failed to get key: %v", err)
}
t.Logf("Got value: %s", val)
}
API
tcredis.RedisV3(t testing.TB) string (⭐ RECOMMENDED)
Creates a Redis-compatible cluster using Dragonfly in emulated cluster mode. This is the most reliable and fastest option for testcontainers.
Implementation: Uses Dragonfly (a high-performance Redis alternative) which presents itself as a Redis cluster to clients, but internally it's a single powerful node. This architecture completely avoids the cluster-announce propagation issues that plague multi-container Redis clusters.
Parameters:
t: The testing.TB instance (used for cleanup). Accepts both *testing.T (unit tests) and *testing.B (benchmarks).
Returns: Connection string in redis://host:port format compatible with redis.ParseClusterURL()
How it works:
- Starts a single Dragonfly container with
--cluster_mode=emulated
- Dragonfly responds to all Redis Cluster protocol commands
- No inter-node communication = no cluster-announce propagation issues
- Verifies readiness before returning
Benchmarks:
- ⚡ Startup time: ~1 second (vs 8s for V2, 22s for V1)
- 🎯 Reliability: 100% in testing (141/141 tests passed)
- 🚀 Performance: 25x faster than Redis
- 📦 Single container: No networking complexity
Ports: Uses port 26379
Trade-offs:
- ✅ Extremely fast and reliable
- ✅ 25x performance improvement over Redis
- ✅ Perfect for most testing scenarios
- ❌ Single node (can't test multi-node cluster scenarios like failover, resharding)
- ❌ Not testing real Redis (different implementation)
When to use:
- Unit and integration tests
- Performance testing
- Any test that needs Redis Cluster protocol but not actual distributed behavior
- When you need speed and reliability
When NOT to use:
- Testing actual multi-node cluster behavior (failover, resharding, replication)
- When you specifically need to test against Redis (not Dragonfly)
tcredis.RedisV2(t testing.TB, nodes int) string
Creates a Redis cluster by starting individual Redis containers and manually forming the cluster. This approach is significantly faster (~8 seconds) than V1.
Parameters:
t: The testing.TB instance (used for cleanup). Accepts both *testing.T (unit tests) and *testing.B (benchmarks).
nodes: Number of master nodes in the cluster (must be >= 3)
Returns: Connection string in redis://host:port format compatible with redis.ParseClusterURL()
How it works:
- Starts N separate Redis containers (one per master node)
- Creates a Docker network for inter-container communication
- Forms the cluster using
redis-cli --cluster create
- Configures
cluster-announce-ip and cluster-announce-port for external client access
- Verifies cluster readiness and returns connection string
Performance: ~8 seconds for a 3-node cluster (vs ~22 seconds for V1)
Ports: Uses ports starting from 7000 (7000, 7001, 7002, etc.)
tcredis.Redis(t testing.TB, nodes int) string
Creates a Redis cluster using the grokzen/redis-cluster Docker image. This is the original implementation.
Parameters:
t: The testing.TB instance (used for cleanup). Accepts both *testing.T (unit tests) and *testing.B (benchmarks).
nodes: Number of master nodes in the cluster (must be >= 3). Each master will have 1 slave by default.
Returns: Connection string in redis://host:port format compatible with redis.ParseClusterURL()
How it works:
- Automatically builds a local Docker image (
tcredis/redis-cluster:7.0.7) on first use
- Starts a single container with the grokzen/redis-cluster image
- The image automatically sets up the cluster with masters and slaves
- Configures
cluster-announce-ip and cluster-announce-port for external client access
- Verifies cluster readiness and returns connection string
Performance: ~22 seconds for a 3-node cluster
Note: The number of nodes specified is the number of masters. The total number of nodes will be 2 * nodes (masters + slaves). For example, Redis(t, 5) creates 5 masters and 5 slaves (10 nodes total).
Ports: Uses ports starting from 10000 (10000 to 10000 + 2 * nodes - 1)
Comparison: RedisV3 vs RedisV2 vs Redis
Benchmark Results
| Metric |
RedisV3 (⭐ RECOMMENDED) |
RedisV2 |
Redis (V1) |
| Implementation |
Dragonfly emulated |
Multi-container Redis |
Single container grokzen |
| Startup Time |
~1 second ⚡ |
~8 seconds |
~22 seconds |
| Reliability |
100% (141/141) |
30-55% (gossip issues) |
100% (single container) |
| Performance |
25x faster than Redis |
Same as Redis |
Same as Redis |
| Nodes |
1 (emulated cluster) |
N masters (no replicas) |
N masters + N replicas |
| Port |
26379 |
27000+ |
10000+ |
| Cleanup |
✅ Perfect |
✅ Perfect |
✅ Perfect |
| Multi-node Testing |
❌ No |
✅ Yes |
✅ Yes |
Reliability Testing Results
Extensive testing with 20-50 iterations per implementation:
RedisV3: 141/141 tests passed = 100.0% ✅⚡
Redis V1: ~100% reliable (single container, no network issues)
RedisV2: 30-55% reliable (Redis gossip protocol non-determinism in Docker)
Startup Time Benchmarks
Average startup time until cluster is ready for client operations:
RedisV3: 0.8-1.8 seconds (22x faster than V1, 8x faster than V2) ⚡
RedisV2: 6-11 seconds (2.7x faster than V1)
Redis V1: 20-24 seconds (baseline)
When to Use Each Implementation
| Use Case |
Recommended |
Why |
| Unit tests |
RedisV3 |
Fastest, most reliable |
| Integration tests |
RedisV3 |
Fast iteration, full cluster protocol support |
| CI/CD pipelines |
RedisV3 |
Speed + reliability = faster builds |
| Multi-node cluster testing |
Redis V1 |
Need real distributed behavior |
| Failover/resharding tests |
Redis V1 |
Need multiple actual nodes |
| Replica behavior testing |
Redis V1 |
Only V1 has replicas |
| Fast multi-node iteration |
RedisV2 |
When V1 too slow, can tolerate flakiness |
Recommendations:
- 🥇 Use
RedisV3 for 95% of tests - fastest and most reliable
- 🥈 Use
Redis (V1) when you specifically need multi-node behavior or replicas
- 🥉 Use
RedisV2 rarely - only when you need multi-node testing and V1 is too slow
Parallel Test Execution
All implementations support parallel test execution without port conflicts. The library uses an internal port allocator that:
- Allocates ports sequentially starting from 27000 (configurable)
- No port reuse - always allocates new sequential ports to avoid Docker async cleanup conflicts
- Thread-safe for concurrent test execution
Why no port reuse? Docker's cleanup is asynchronous. Even after container.Terminate() returns, Docker may spend several seconds cleaning up network bindings. Reusing ports too quickly causes "port is already allocated" errors.
Customizing Ports
You can customize ports in two ways:
1. Per-test (Recommended) - Using functional options:
// RedisV3 with custom port
connStr := tcredis.RedisV3(t, tcredis.WithStartingPort(38000))
// RedisV2 with custom port range
connStr := tcredis.RedisV2(t, 3, tcredis.WithStartingPortV2(39000))
// Uses ports 39000, 39001, 39002
2. Global - Using SetStartingPort() in TestMain:
func TestMain(m *testing.M) {
// All tests will use ports starting from 37000
tcredis.SetStartingPort(37000)
os.Exit(m.Run())
}
Recommendation: Use functional options (WithStartingPort) for flexibility. Use SetStartingPort() only if you need to change the default for all tests.
Port Allocation Behavior
- RedisV3: Allocates 1 port per instance (reused after cleanup)
- RedisV2: Allocates N contiguous ports for N nodes (e.g., 27000-27002 for 3 nodes)
- Redis (V1): Allocates 2*N ports for N masters + N replicas (e.g., 10000-10005 for 3 masters)
Example parallel execution:
// Test 1 gets port 27000
// Test 2 gets port 27001 (parallel or sequential)
// Test 3 gets port 27002
// Test 4 gets port 27003
// etc.
// Ports are NOT reused to avoid Docker async cleanup conflicts
Requirements
- Go 1.21 or later
- Docker (required by testcontainers-go)
Both functions return connection strings in the format:
redis://host:port?addr=host:port2&addr=host:port3
This format is compatible with redis.ParseClusterURL() from the github.com/redis/go-redis/v9 package.
Automatic Image Building (Redis V1 only)
The Redis function automatically builds a local Docker image (tcredis/redis-cluster:7.0.7) on first use. This ensures:
- Better ARM64 compatibility: No emulation needed on Apple Silicon
- Faster subsequent runs: Image is cached after first build
- More reliable: Locally built images work better with testcontainers
First run: The library will automatically:
- Check if
tcredis/redis-cluster:7.0.7 exists locally
- If not, clone the grokzen/docker-redis-cluster repository (cached in
/tmp/docker-redis-cluster)
- Build the image for your native platform (arm64 on Apple Silicon, amd64 on Intel)
- Use the locally built image
Subsequent runs: The image is already built, so tests start immediately.
The repository is cached in /tmp/docker-redis-cluster and will be updated automatically when needed.
Note: RedisV2 uses the official redis:7.0.7 image which is pulled automatically if not present.
Cluster Requirements
- Minimum nodes: Redis clusters require at least 3 master nodes to form a proper cluster with hash slot assignment. Both
Redis and RedisV2 enforce this requirement and will fail with a clear error message if fewer than 3 nodes are requested.
Testing
Run the tests:
go test ./...
Make sure Docker is running before executing tests.
Examples
Using RedisV3 (⭐ Recommended)
func TestRedisCluster(t *testing.T) {
connStr := tcredis.RedisV3(t) // Dragonfly emulated cluster - fast and reliable!
opts, _ := redis.ParseClusterURL(connStr)
client := redis.NewClusterClient(opts)
defer client.Close()
ctx := context.Background()
client.Set(ctx, "key", "value", 0)
val, _ := client.Get(ctx, "key").Result()
t.Logf("Value: %s", val)
}
Using RedisV2
func TestRedisCluster(t *testing.T) {
connStr := tcredis.RedisV2(t, 3) // 3-node cluster
opts, _ := redis.ParseClusterURL(connStr)
client := redis.NewClusterClient(opts)
defer client.Close()
ctx := context.Background()
client.Set(ctx, "key", "value", 0)
val, _ := client.Get(ctx, "key").Result()
t.Logf("Value: %s", val)
}
Using Redis (V1)
func TestRedisCluster(t *testing.T) {
connStr := tcredis.Redis(t, 3) // 3 masters + 3 slaves = 6 nodes total
opts, _ := redis.ParseClusterURL(connStr)
client := redis.NewClusterClient(opts)
defer client.Close()
ctx := context.Background()
client.Set(ctx, "key", "value", 0)
val, _ := client.Get(ctx, "key").Result()
t.Logf("Value: %s", val)
}
License
MIT