Cache Cloud Run Reads with Memorystore for Redis
Learn how to add a Redis cache to a Cloud Run service, reduce DB load, and measure hit ratios. Includes example, trade-offs, and a verification checklist.
25 Jan 2026, 23:39 UTC

The Problem: Database Round-Trips on Every Request
Cloud Run scales horizontally, but each instance still opens connections to Cloud SQL. When a read-heavy endpoint is called repeatedly, every request pays for a database round-trip. Memorystore for Redis is a managed in-memory store that can absorb those repeated reads inside your VPC, reducing latency and connection pressure on the database.
Pattern: Cache-First Reads with Write-Through Updates
A common approach is cache-first with write-through:
- Read: Check Redis first. If the key exists, return it.
- Miss: Query Cloud SQL, store the result in Redis with a TTL, then return it.
- Write: Update Cloud SQL first, then update or delete the Redis key so the next read repopulates it.
TTLs bound staleness automatically. Redis also evicts keys under memory pressure, so the application must tolerate misses. Memorystore does not persist data across restarts; treat it strictly as a cache, never as the system of record.
Worked Example: Python Service on Cloud Run
The following Flask handler demonstrates the pattern. Placeholders are shown in angle brackets. Run the deployment commands from Cloud Shell or a terminal with the Cloud SDK installed; you need roles/redis.admin and roles/run.admin (or equivalent) in the project.
import os
import json
from flask import Flask, jsonify
import redis
import pymysql
app = Flask(__name__)
# Redis client; the library manages a connection pool internally.
r = redis.Redis(
host=os.environ["REDIS_HOST"],
port=int(os.environ.get("REDIS_PORT", "6379")),
decode_responses=True,
)
# Cloud SQL connection settings come from environment variables.
sql_conn = pymysql.connect(
user=os.environ["SQL_USER"],
password=os.environ["SQL_PASSWORD"],
unix_socket=os.environ["SQL_SOCKET"], # /cloudsql/::
database=os.environ["SQL_DB"],
cursorclass=pymysql.cursors.DictCursor,
)
CACHE_TTL = 300 # seconds
@app.route("/user/")
def get_user(user_id):
key = f"user:{user_id}"
val = r.get(key)
if val:
return jsonify(source="cache", data=json.loads(val))
with sql_conn.cursor() as cur:
cur.execute(
"SELECT id, name, email FROM users WHERE id=%s", (user_id,)
)
row = cur.fetchone()
if not row:
return jsonify(error="not found"), 404
r.setex(key, CACHE_TTL, json.dumps(row))
return jsonify(source="db", data=row)
Create the Memorystore instance and a Serverless VPC Access connector so Cloud Run can reach the private Redis IP. Memorystore does not support public IPs.
# Create a VPC connector in the same region as the Cloud Run service.
gcloud compute networks vpc-access connectors create \
--region \
--range # e.g., 10.8.0.0/28
# Create a Basic-tier Redis instance.
gcloud redis instances create \
--size \
--region \
--network \
--connect-mode=PRIVATE_SERVICE_ACCESS \
--redis-version redis_7_0
# Retrieve the private IP address.
gcloud redis instances describe \
--region \
--format="value(host)"
Deploy the service with the connector and environment variables:
gcloud run deploy \
--image \
--region \
--vpc-connector \
--set-env-vars REDIS_HOST=,REDIS_PORT=6379 \
--set-secrets SQL_USER=:latest,SQL_PASSWORD=:latest
Replace <IMAGE_URL> with your Artifact Registry path. Use Secret Manager for database credentials rather than inline environment variables where possible.
Trade-offs and Limitations
- Volatility: Redis data is lost on restart or eviction. Always fall back to Cloud SQL.
- Stateless clients: Cloud Run instances are ephemeral. The Redis client must be thread-safe and connection-pooled; creating a new connection per request will exhaust file descriptors.
- Invalidation risk: If write-through is skipped or fails after the database commit, stale keys persist until TTL expiry. Prefer deleting the key over updating it when consistency matters more than latency.
- Network path: Connections traverse the VPC connector or Direct VPC egress. Misconfigured firewall rules or an unavailable connector cause connection timeouts, not clean errors.
- Cost drivers: Instance memory size and replica count dominate Memorystore pricing. Cross-region access adds egress cost; keep the instance in the same region as the Cloud Run service.
How to Verify the Change
- Baseline: Deploy without Redis and record p95 latency and Cloud SQL active connections in Cloud Monitoring.
- Connectivity check: From a temporary VM in the same VPC, run
redis-cli -h ping. ExpectPONG. This isolates VPC routing from application bugs. - Miss behavior: Call the endpoint twice. The first response should show
"source": "db"; the second should show"source": "cache". - TTL expiry: Wait for the TTL to lapse or delete the key with
DEL user:. Confirm the next request falls back to the database and repopulates the cache. - Monitoring: Add the Memorystore cache hit ratio metric to a Cloud Monitoring dashboard. Compare it against the baseline latency graph to confirm the cache is absorbing load.
Bottom Line
Memorystore for Redis is a good fit for Cloud Run services that repeatedly read the same rows from Cloud SQL. Start with a Basic-tier instance sized to your working set, use short TTLs, and verify connectivity from inside the VPC before debugging application code. Treat every cache miss as an expected path, not an error.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.