Portal Performance & Caching

A developer portal that takes three seconds to render an entity page trains engineers to stop using it, so performance is a retention feature, not a nice-to-have. This guide is for platform engineers who need the catalog, documentation, and search surfaces of an internal portal to stay fast as entity counts and concurrency grow, using layered caching, database tuning, and honest measurement rather than guesswork.

Layered caching in front of the portal backend A request passes a CDN edge cache, then a Redis application cache, before reaching the API tier and a primary database with read replicas. Browser SPA CDN edge static assets Redis cache catalog reads API tier BFF Primary DB writes Read replicas catalog reads
Each layer absorbs the requests the one behind it would otherwise serve, so the database only sees genuine cache misses.

Performance work on a portal is layered by design: the CDN answers for static assets, an application cache answers for hot catalog reads, and only true misses reach the database. The mistake teams make is optimizing one layer in isolation — adding a Redis cache while the CDN serves nothing, or tuning the database while every request bypasses the cache. This guide treats them as a stack, and it assumes the decoupled architecture established in Choosing the Right Framework and the scaling boundaries from the broader Developer Portal Architecture & Frameworks section.

Prerequisites & Environment Baseline

Before adding a cache, measure. A cache placed in front of an already-fast path adds complexity and a new failure mode without a win, and a cache with the wrong invalidation strategy serves dangerously stale ownership data. Establish a baseline runtime, a metrics pipeline that reports p95 latency per endpoint, and a cache store before touching configuration.

Measurement and infrastructure baseline before caching A latency baseline, a metrics pipeline, and a provisioned cache store must exist before caching configuration. latency baseline metrics pipeline cache store ready Safe to add caching measured, not guessed
A p95-per-endpoint baseline is what lets you prove a cache helped rather than assuming it did.

The baseline you need:

  • Runtime: Node.js 20+ with the portal backend already serving traffic; a ${REDIS_URL} reachable from the backend for the application cache.
  • Metrics: request duration histograms per route, exported to Prometheus, so you can read a real p95 before and after each change.
  • A slow-query log enabled on the catalog database, because caching hides slow queries rather than fixing them, and you want to know which queries are genuinely expensive.
# Requires Node.js >= 20, redis-cli >= 7
redis-cli -u ${REDIS_URL} ping          # expect: PONG
node -e "console.log(process.version)"  # expect: v20.x or higher

Step-by-Step Configuration & Plugin Architecture

Configure the layers from the outside in: CDN cache-control on static assets first (the cheapest win), then an application cache for catalog reads with a bounded TTL and webhook-triggered invalidation, then database read replicas for the queries that still reach it. Each layer has a distinct invalidation model, and getting invalidation right matters more than the cache itself.

Configuring the cache layers outside-in CDN cache-control, then an application cache with TTL and invalidation, then database read replicas. 1. CDN cache-control immutable assets 2. App cache TTL + invalidation 3. Read replicas offload the primary
Do the CDN layer first — it is the cheapest change and often the largest single improvement to perceived load time.

Configure the backend to use Redis as its cache store and give catalog reads a short, bounded TTL. The mechanics of a catalog-specific cache are covered in Configuring Redis Caching for the Catalog.

# app-config.yaml
# Requires Backstage >= 1.20.0
backend:
  cache:
    store: redis
    connection: ${REDIS_URL}
    defaultTtl: { minutes: 10 }   # bounded; webhook busts on write

Serve fingerprinted static assets from the CDN with a long immutable lifetime while keeping HTML revalidating, so a deploy is visible immediately — the exact header split is detailed in Setting CDN Cache Headers for Portal Assets.

Validation & Health Checks

Prove the cache is working, not merely present: confirm a warm read hits Redis, that the hit ratio is healthy, and that a write busts the entry so ownership data never goes stale.

Cache validation checks A warm read hits the cache, the hit ratio is healthy, and a write invalidates the entry. Warm read hits second call fast Hit ratio healthy keyspace_hits Write invalidates no stale reads
The invalidation check is the one that matters most — a fast cache that serves stale owners is worse than a slow correct one.
# Requires redis-cli >= 7
# Hit ratio: hits / (hits + misses)
redis-cli -u ${REDIS_URL} info stats | grep -E "keyspace_hits|keyspace_misses"
# A healthy read-heavy portal shows hits far exceeding misses.

# Confirm a catalog write busts the cached entry
curl -s -X POST ${PORTAL_API_URL}/api/catalog/refresh \
  -H "Authorization: Bearer ${PORTAL_TOKEN}" \
  -d '{"entityRef":"component:default/my-service"}'
redis-cli -u ${REDIS_URL} exists "catalog:component:default/my-service"
# Expected: 0 (entry evicted on write)

Maintenance & Lifecycle Management

Caching is not set-and-forget; it needs the same operational care as any stateful dependency. Watch for cache stampedes when a hot key expires, keep the hit ratio and memory usage on a dashboard, and have a documented path to run cache-free during an incident.

Cache operational maintenance Guard against stampedes, monitor hit ratio and memory, and keep a cache-free fallback path. Prevent stampede jittered TTL + lock Monitor hit ratio + memory Cache-free path incident fallback
A documented cache-free mode turns a Redis outage into degraded performance rather than a portal outage.
  • Stampede protection: add jitter to TTLs and use a short lock so one request repopulates a hot key while others wait, instead of a thundering herd hitting the database.
  • Dashboards: alert on hit ratio dropping below your baseline and on Redis memory nearing maxmemory, which triggers evictions that silently degrade the ratio.
  • Graceful degradation: the backend must treat a cache miss and a cache outage identically, falling through to the database so a Redis failure slows the portal rather than breaking it.

Common Pitfalls & Mitigation Strategies

The performance failures cluster around two roots: caching without invalidation (fast but wrong) and optimizing a layer that was never the bottleneck (effort with no win). The map below pairs each pitfall with its fix.

Performance pitfalls by root cause Stale reads and cache stampedes stem from invalidation gaps; premature caching and unindexed queries stem from unmeasured bottlenecks. Invalidation gaps stale reads cache stampede → bust + jitter Unmeasured bottleneck premature cache unindexed query → measure + index
Measure first and invalidate on write — those two habits prevent the majority of portal performance regressions.
  • Serving stale ownership after a write. Fix: bust the cache key on every catalog write via a webhook, and keep TTLs short as a backstop.
  • Cache stampede when a hot key expires. Fix: jitter TTLs and guard repopulation with a short lock.
  • Adding a cache before measuring. Fix: profile with the slow-query log and p95 histograms; cache only the paths that are actually slow and hot.
  • Caching a query that should be indexed. Fix: run EXPLAIN ANALYZE, add the missing index, and re-measure before deciding a cache is warranted.

Frequently Asked Questions

Should I cache in Redis or rely on the database’s own cache?

Use both, at different layers. The database’s buffer cache speeds up repeated reads of the same pages, but it still requires a query round-trip and query planning. An application cache in Redis returns a pre-serialized response with no database contact at all, which is what keeps latency flat under high concurrency. Reach for Redis once a single database can no longer absorb the read load, and keep queries indexed regardless.

What TTL should catalog entries use?

Short — five to fifteen minutes — combined with webhook-driven invalidation on write. The TTL is a backstop that bounds how stale a read can be if an invalidation event is ever missed; the webhook is what keeps normal operation fresh. A long TTL with no invalidation is how portals end up showing a service’s previous owner for an hour.

How do I know caching actually helped?

Compare the p95 latency of the cached endpoints before and after, under representative load, and watch the database’s read QPS drop. If p95 did not improve or the database load did not fall, the cache is either not being hit or was placed in front of a path that was already fast — in which case remove it.