Redis-Cached Session Context & Responsive S3 Images

Web & API Engineering intermediate Building with AI ~18 min read

Overview

Server-rendered SaaS applications need two things on every page load: the identity and permissions of the current user, and any dynamic assets (like profile images or uploaded content) rendered at the right resolution for the visitor’s device. A web application solves both problems through template helpers that hydrate a ContextUser and ContextAccount from Redis-backed session IDs and lazily generate responsive S3 image URLs via an img_resize helper that produces cached srcset attributes for server-rendered HTML.

The session layer works as follows: when a user logs in, the server creates a session ID, stores the user’s profile and account data in Redis under that key with a TTL, and sets a signed cookie containing the session ID. On subsequent requests, middleware reads the cookie, fetches the session data from Redis, and populates the template context with ContextUser (name, email, role, permissions) and ContextAccount (plan, quota, feature flags). Template helpers access these objects directly, avoiding a database round-trip on every page render.

The image pipeline addresses a different performance concern. User-uploaded images stored in S3 are often multi-megabyte originals. Serving them directly wastes bandwidth and makes pages slow on mobile. The img_resize template helper generates URLs for an image resizing service (or S3-backed Lambda@Edge function) at multiple widths and assembles them into an HTML srcset attribute. The generated srcset is cached in Redis so that repeated renders of the same image don’t recompute the URL list. The browser then selects the appropriately sized image based on the viewport width and device pixel ratio.

Core Concepts

Redis-Backed Sessions

Storing session data in Redis instead of a relational database provides sub-millisecond reads, automatic expiration via TTL, and horizontal scalability. Each session is a hash or serialized JSON blob keyed by the session ID. Redis’s single-threaded event loop ensures that session reads and writes are atomic, and its persistence options (RDB snapshots, AOF) provide durability if the server restarts. Multiple application servers share the same Redis instance, enabling stateless horizontal scaling—any server can serve any user’s request.

ContextUser and ContextAccount Hydration

Rather than passing raw session data to templates, the middleware constructs typed ContextUser and ContextAccount structs that contain exactly the fields templates need. This layer converts the flat Redis hash into domain objects with computed properties like IsAdmin(), CanAccessFeature(name), and PlanQuotaRemaining(). Templates reference these methods directly, keeping business logic out of template code and ensuring consistent permission checks across all pages.

Responsive Images with srcset

The HTML srcset attribute tells the browser about multiple versions of the same image at different widths. The browser selects the most appropriate version based on the viewport size, device pixel ratio, and network conditions. A srcset like img-400.jpg 400w, img-800.jpg 800w, img-1200.jpg 1200w combined with a sizes attribute lets the browser download a 400px image on mobile and a 1200px image on desktop, reducing bandwidth by 60-80% for mobile users.

Lazy Image URL Generation with Caching

The img_resize helper generates resize URLs on first call and caches the result in Redis. The cache key includes the S3 object key and the set of target widths, so the same image rendered at the same sizes on different pages reuses the cached srcset string. This avoids redundant URL construction (which may involve signing S3 URLs with expiration timestamps) and keeps template rendering fast even when a page displays dozens of images.

Signed Cookies for Session ID Transport

The session ID is stored in an HTTP cookie signed with HMAC-SHA256. Signing prevents tampering—a user who modifies the cookie value invalidates the signature, and the server rejects the request. The cookie is marked HttpOnly (inaccessible to JavaScript, preventing XSS theft), Secure (only sent over HTTPS), and SameSite=Lax (mitigates CSRF attacks). The session ID itself is a cryptographically random string (128+ bits of entropy) that reveals no information about the user.

How It Works

Session Middleware

The session middleware runs on every request. It reads the session cookie, fetches the session data from Redis, and populates the request context with the user and account objects. If the session is missing or expired, the context contains a nil user, and templates render the logged-out state.

package middleware

import (
    "context"
    "encoding/json"
    "net/http"
    "time"

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

type SessionData struct {
    UserID      string   `json:"user_id"`
    Email       string   `json:"email"`
    Name        string   `json:"name"`
    Role        string   `json:"role"`
    AccountID   string   `json:"account_id"`
    AccountPlan string   `json:"account_plan"`
    Features    []string `json:"features"`
}

type ContextUser struct {
    ID    string
    Email string
    Name  string
    Role  string
}

func (u *ContextUser) IsAdmin() bool {
    return u.Role == "admin" || u.Role == "owner"
}

type ContextAccount struct {
    ID       string
    Plan     string
    Features map[string]bool
}

func (a *ContextAccount) CanAccessFeature(name string) bool {
    return a.Features[name]
}

func SessionMiddleware(rdb *redis.Client) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            cookie, err := r.Cookie("session_id")
            if err != nil {
                next.ServeHTTP(w, r)
                return
            }

            sessionID, err := verifySignedCookie(cookie.Value)
            if err != nil {
                next.ServeHTTP(w, r)
                return
            }

            raw, err := rdb.Get(r.Context(), "session:"+sessionID).Bytes()
            if err != nil {
                next.ServeHTTP(w, r)
                return
            }

            var data SessionData
            if err := json.Unmarshal(raw, &data); err != nil {
                next.ServeHTTP(w, r)
                return
            }

            // Refresh TTL on active sessions
            rdb.Expire(r.Context(), "session:"+sessionID, 24*time.Hour)

            featureMap := make(map[string]bool, len(data.Features))
            for _, f := range data.Features {
                featureMap[f] = true
            }

            ctx := r.Context()
            ctx = context.WithValue(ctx, userCtxKey, &ContextUser{
                ID: data.UserID, Email: data.Email,
                Name: data.Name, Role: data.Role,
            })
            ctx = context.WithValue(ctx, accountCtxKey, &ContextAccount{
                ID: data.AccountID, Plan: data.AccountPlan,
                Features: featureMap,
            })

            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Session Creation on Login

When a user authenticates successfully, the login handler creates a session in Redis and sets the signed cookie. The session data includes everything the templates need so that subsequent requests avoid database queries.

func handleLogin(rdb *redis.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        email := r.FormValue("email")
        password := r.FormValue("password")

        user, account, err := authenticateUser(email, password)
        if err != nil {
            http.Error(w, "Invalid credentials", http.StatusUnauthorized)
            return
        }

        sessionID := generateSecureRandomID(32)

        data := SessionData{
            UserID:      user.ID,
            Email:       user.Email,
            Name:        user.Name,
            Role:        user.Role,
            AccountID:   account.ID,
            AccountPlan: account.Plan,
            Features:    account.EnabledFeatures,
        }

        raw, _ := json.Marshal(data)
        rdb.Set(r.Context(), "session:"+sessionID, raw, 24*time.Hour)

        signed := signCookie(sessionID)
        http.SetCookie(w, &http.Cookie{
            Name:     "session_id",
            Value:    signed,
            Path:     "/",
            HttpOnly: true,
            Secure:   true,
            SameSite: http.SameSiteLaxMode,
            MaxAge:   86400,
        })

        http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
    }
}

Responsive Image Template Helper

The img_resize template function generates a complete <img> tag with srcset and sizes attributes. It checks Redis for a cached srcset string before generating one, and caches the result for future renders.

package helpers

import (
    "context"
    "fmt"
    "html/template"
    "strings"
    "time"

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

var defaultWidths = []int{320, 640, 960, 1280, 1920}

type ImageHelper struct {
    rdb       *redis.Client
    cdnBase   string
    s3Bucket  string
}

func (h *ImageHelper) ImgResize(s3Key, alt, sizes string) template.HTML {
    cacheKey := fmt.Sprintf("srcset:%s:%v", s3Key, defaultWidths)

    cached, err := h.rdb.Get(context.Background(), cacheKey).Result()
    if err == nil {
        return template.HTML(fmt.Sprintf(
            `<img srcset="%s" sizes="%s" src="%s" alt="%s" loading="lazy">`,
            cached, sizes, h.imageURL(s3Key, 960), alt,
        ))
    }

    var parts []string
    for _, w := range defaultWidths {
        url := h.imageURL(s3Key, w)
        parts = append(parts, fmt.Sprintf("%s %dw", url, w))
    }
    srcset := strings.Join(parts, ", ")

    h.rdb.Set(context.Background(), cacheKey, srcset, 6*time.Hour)

    return template.HTML(fmt.Sprintf(
        `<img srcset="%s" sizes="%s" src="%s" alt="%s" loading="lazy">`,
        srcset, sizes, h.imageURL(s3Key, 960), alt,
    ))
}

func (h *ImageHelper) imageURL(s3Key string, width int) string {
    return fmt.Sprintf("%s/resize/%d/%s/%s", h.cdnBase, width, h.s3Bucket, s3Key)
}

func TemplateFuncMap(rdb *redis.Client, cdnBase, bucket string) template.FuncMap {
    helper := &ImageHelper{rdb: rdb, cdnBase: cdnBase, s3Bucket: bucket}
    return template.FuncMap{
        "img_resize": helper.ImgResize,
    }
}

Using Helpers in Templates

Server-rendered templates access the session context and image helper directly. The template engine receives the context user and account as part of the template data, and the img_resize function is registered in the template function map.

{{/* dashboard.html */}}
{{ if .User }}
  <nav>
    <span>Welcome, {{ .User.Name }}</span>
    {{ if .User.IsAdmin }}
      <a href="/admin">Admin Panel</a>
    {{ end }}
    {{ if .Account.CanAccessFeature "advanced-search" }}
      <a href="/search/advanced">Advanced Search</a>
    {{ end }}
    <span class="plan-badge">{{ .Account.Plan }}</span>
  </nav>

  <div class="profile">
    {{ img_resize .User.AvatarKey "Profile photo" "(max-width: 600px) 100vw, 300px" }}
  </div>

  <div class="gallery">
    {{ range .Documents }}
      <div class="card">
        {{ img_resize .ThumbnailKey .Title "(max-width: 600px) 50vw, 25vw" }}
        <h3>{{ .Title }}</h3>
      </div>
    {{ end }}
  </div>
{{ else }}
  <a href="/login">Sign In</a>
{{ end }}

Practical Applications

The web app demonstrates a session architecture that scales horizontally without sticky sessions. Because all session state lives in Redis rather than in-process memory, any application server can handle any request. Load balancers can distribute traffic purely on health and capacity, and deploying a new server version is a rolling restart with zero session loss. The TTL-based expiration in Redis eliminates the need for a session cleanup cron job—expired sessions simply vanish.

The responsive image pipeline reduces page weight by 60-80% on mobile devices without requiring developers to manually create multiple image sizes. The img_resize helper is a single function call in the template that handles URL generation, caching, and HTML output. When the CDN or image service URL format changes, a single code change in the helper propagates to every image on every page. The Redis cache prevents URL generation from becoming a bottleneck on pages with many images.

The combination of these two patterns creates a fast, consistent server-rendered experience. Every page load fetches the session from Redis (sub-millisecond), hydrates the user and account context, and renders templates that include optimally-sized images. The total server-side rendering time stays under 10ms even on pages with complex permission checks and dozens of images, because the hot data is always in Redis and the image URLs are cached.

Implementation Patterns

Session Data Denormalization

The session stores denormalized user and account data (name, email, plan, features) rather than just a user ID. This eliminates a database query on every request at the cost of potentially stale data. When the user updates their profile or the account plan changes, the application must update the session in Redis as well as the database. A helper function refreshSession(ctx, userID) fetches fresh data from the database and overwrites the Redis session, keeping both stores consistent.

Signed URL Rotation for S3 Images

If the image resizing service uses S3 presigned URLs, those URLs expire after a configured period. The Redis cache TTL for srcset strings must be shorter than the presigned URL expiration to prevent serving expired URLs. A common configuration is 1-hour presigned URLs with a 45-minute cache TTL, giving a 15-minute buffer. Public buckets behind CloudFront don’t need signed URLs and can use longer cache TTLs.

Graceful Degradation on Redis Failure

If Redis is unavailable, the session middleware should not crash the application. Instead, it should treat the request as unauthenticated (no session found) and let the page render in the logged-out state. Similarly, the image helper should fall back to the original S3 URL at a default width rather than returning an error. This degradation is transparent to the user on public pages and results in a login prompt on authenticated pages.

Common Pitfalls

Further Reading