Connect to Redis

Use the official `go-redis/redis` library to connect to Redis by initializing a client with your server address and authentication details, then verify the connection with a simple `Ping` command.

Your database is the bottleneck

Your Go service is handling traffic. The latency is creeping up. You check the metrics and see the database CPU spiking on every request. You decide to add Redis as a cache layer. You know how to write HTTP handlers. You know how to query Postgres. Now you need to talk to a key-value store that lives in memory.

Go doesn't ship with a Redis driver in the standard library. The standard library gives you net/http and database/sql, but Redis speaks its own protocol. You need a client library. The Go ecosystem has converged on one library for this job: github.com/redis/go-redis. It handles the protocol, manages connection pooling, and maps Redis types to Go types. You write Go code. The library translates it to Redis commands.

How the client works

Redis is a network service. Every interaction requires a TCP connection. Opening a connection costs time. The handshake, the TLS negotiation if you use it, the authentication. If you open a new connection for every request, your latency will suffer.

The go-redis client solves this with a connection pool. When you create a client, you configure a pool of connections. The pool keeps connections alive and reuses them. When you call Get or Set, the library grabs a connection from the pool, sends the command, reads the response, and returns the connection to the pool. You never manage the connections yourself. The client is thread-safe. You create one client instance and share it across all goroutines in your application.

The library also enforces a strict pattern for errors. Redis commands return a command object, not the value directly. You must call .Err() to check for errors or .Result() to get the value. This design makes it impossible to accidentally ignore an error. The compiler forces you to handle the return value.

Convention aside: Go functions that interact with external resources always take a context.Context as the first parameter. The go-redis library follows this rule. Every method like Get, Set, and Ping starts with ctx. This allows the library to respect cancellation and deadlines. If your HTTP handler times out, the context cancels, and the Redis call aborts.

Minimal connection example

This example creates a client, verifies the connection, writes a value, reads it back, and cleans up. It uses the v9 API, which is the current stable version.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/redis/go-redis/v9"
)

// Main demonstrates a basic Redis round-trip with error handling.
func main() {
	// Context carries cancellation and deadlines.
	// Pass context to every Redis call so the library can timeout.
	ctx := context.Background()

	// NewClient initializes the client with a connection pool.
	// The pool is configured with sensible defaults for most applications.
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	// Ping checks if the server is reachable.
	// This is a cheap way to verify connectivity before starting work.
	if err := rdb.Ping(ctx).Err(); err != nil {
		log.Fatalf("connection failed: %v", err)
	}

	// Set writes a key with a time-to-live.
	// The TTL ensures stale data doesn't accumulate forever.
	if err := rdb.Set(ctx, "greeting", "Hello, Go!", time.Hour).Err(); err != nil {
		log.Fatalf("write failed: %v", err)
	}

	// Get retrieves the value.
	// Result returns the typed value and any error.
	val, err := rdb.Get(ctx, "greeting").Result()
	if err != nil {
		log.Fatalf("read failed: %v", err)
	}

	fmt.Println("Value from Redis:", val)

	// Close releases the connection pool resources.
	// Call this once during application shutdown.
	if err := rdb.Close(); err != nil {
		log.Printf("close failed: %v", err)
	}
}

Run this code with a local Redis instance. The Ping call triggers the first connection. The pool starts with zero connections and grows as needed. Set and Get reuse the connection. Close drains the pool and closes all sockets.

The client is a resource. Create it once. Share it everywhere.

Realistic caching pattern

In production, you rarely call Redis directly from main. You wrap it in a service or pass it to handlers. This example shows a cache-aside pattern. The function checks the cache first. If the data is missing, it fetches from a simulated database and populates the cache.

package service

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"time"

	"github.com/redis/go-redis/v9"
)

// UserCache handles caching logic for user data.
// The receiver name s is short for service, following Go convention.
type UserCache struct {
	rdb *redis.Client
}

// NewUserCache creates a cache service with a shared Redis client.
func NewUserCache(rdb *redis.Client) *UserCache {
	return &UserCache{rdb: rdb}
}

// GetUser retrieves a user by ID, checking the cache first.
// It returns the user data or an error if both cache and DB fail.
func (s *UserCache) GetUser(ctx context.Context, id string) (string, error) {
	// Try cache first.
	// This avoids hitting the database for frequent requests.
	val, err := s.rdb.Get(ctx, "user:"+id).Result()
	if err == nil {
		return val, nil
	}

	// Handle cache miss.
	// redis.Nil is the specific error for missing keys.
	// Other errors indicate network issues or server failures.
	if err != redis.Nil {
		return "", fmt.Errorf("cache error: %w", err)
	}

	// Simulate database lookup.
	// In real code, this would be a query to your primary database.
	user := fmt.Sprintf("user-data-%s", id)

	// Populate cache with the fresh data.
	// Add random jitter to the TTL to prevent thundering herd on expiry.
	ttl := time.Minute + time.Duration(rand.Intn(60))*time.Second
	if err := s.rdb.Set(ctx, "user:"+id, user, ttl).Err(); err != nil {
		// Log the cache write failure but return the data.
		// The request succeeds even if caching fails.
		// This is the "best effort" cache strategy.
		log.Printf("cache write failed: %v", err)
	}

	return user, nil
}

The code checks for redis.Nil explicitly. This is the standard way to handle missing keys. If you treat redis.Nil as a generic error, you'll retry the database unnecessarily or return errors to the user when the key is simply not there yet. The TTL jitter prevents a thundering herd. If you set a fixed TTL of one minute, all keys expire at the same time. The next request triggers a database hit for every key. Adding random jitter spreads the expirations out.

Convention aside: Receiver names are usually one or two letters matching the type. (s *UserCache) is idiomatic. (this *UserCache) or (self *UserCache) are not. The community expects short names. Also, gofmt is mandatory. Run it on save. Don't argue about indentation. The tool decides.

Pitfalls and errors

Redis clients introduce specific failure modes. Knowing these patterns saves debugging time.

Missing keys return redis.Nil. The compiler won't catch this. At runtime, Get returns an error when the key doesn't exist. You must check err == redis.Nil. If you skip this check, your code treats a cache miss as a failure. The error message is redis: nil.

Context cancellation stops the call. If you pass a context with a deadline, the library respects it. If the deadline passes, the call returns context.DeadlineExceeded. This is good. It prevents goroutines from hanging forever. However, if you don't pass a context, the call blocks until the server responds or the connection drops. Always pass context. The compiler rejects rdb.Get("key") with not enough arguments in call to rdb.Get because the context is mandatory in v9.

Connection pool exhaustion. If your application opens more concurrent requests than the pool allows, requests block waiting for a connection. The default pool size is 10 connections per CPU core. High-traffic services may need tuning. You can adjust MaxIdleConns and MaxActiveConns in the options. MaxIdleConns controls how many connections stay open when idle. MaxActiveConns caps the total connections. If you hit the cap, the client returns redis: connection pool reserve timeout.

Ignoring Close. The client holds open sockets. If you create a client and never close it, the sockets leak. In a long-running service, this isn't a problem if you create the client once. In tests, you must close the client. The compiler doesn't warn about resource leaks. You have to manage the lifecycle.

Wrong types. Redis is flexible. Go is strict. If you store a string and try to increment it as an integer, Redis returns an error. The library propagates this as WRONGTYPE Operation against a key holding the wrong kind of value. The compiler won't catch this. You get the error at runtime.

The worst Redis bug is the one that silently returns stale data. Always check errors. Always handle redis.Nil.

Tuning the connection pool

The default pool settings work for most applications. High-traffic services or services with unstable networks need tuning.

ConnMaxIdleTime controls how long an idle connection stays in the pool. If a connection sits unused for this duration, the client closes it. This prevents stale connections. Firewalls or proxies sometimes drop idle TCP connections. If the client doesn't know, it tries to use a dead connection and gets an error. Setting ConnMaxIdleTime to less than the firewall timeout avoids this. A value of 30 seconds is common.

ConnMaxLifetime forces connection rotation. Even if a connection is healthy, the client closes it after this duration. Long-lived connections can accumulate state or hit limits on the server side. Rotating them ensures fresh connections. A value of 5 to 10 minutes is typical.

MaxIdleConns determines the pool size when the load drops. If you set this too low, the client closes connections aggressively. The next request pays the cost of a new handshake. If you set it too high, you waste file descriptors and memory. Match this to your baseline load.

Convention aside: The if err != nil { return err } pattern is verbose by design. The Go community accepts the boilerplate because it makes the unhappy path visible. Don't try to hide errors. Return them. Let the caller decide.

When to use what

Use go-redis when you need a full-featured client with connection pooling, type safety, and context support. Use redis.NewClusterClient when your data is sharded across multiple nodes and you need automatic routing. Use redis.NewUniversalClient when you want code that works for both standalone and cluster with a single configuration switch. Use raw TCP or a minimal library when you have extreme performance requirements and can manage the protocol and pooling yourself. Use an in-memory map when you don't need persistence or cross-process sharing.

Redis is fast. The network is the bottleneck. Keep the client shared. Keep the context flowing. Trust the pool.

Where to go next