Skip to content

perf: cache Signal Feed for instant loads #36

@michaelzwang13

Description

@michaelzwang13

Problem

Every open of /agents fires three live external API calls in parallel:

  • Slack (get_slack_messages in backend/app/routers/gateway.py): conversations.list → 5× conversations.historyusers.info lookups
  • Gmail (get_gmail_messages): messages.list + up to 10× messages.get(format=full), with inline OAuth refresh
  • GitHub (get_github_activity): /user + /notifications + /search/issues + /users/{login}/received_events

Aggregate perceived latency ~2–5s on every page open. Nothing is cached — frontend calls in app/src/lib/api.ts:127-140 go straight through to the backend, and the backend goes straight to the external API.

Goal

Subsequent loads within the cache TTL return in <100ms. Cold start still pays the external-API cost once.

Approach

In-memory TTL cache on the backend (module-level dict, ~180s TTL). Cache-aside pattern wired into all three feed handlers. Production-grade equivalent would be Redis (shared across uvicorn workers, survives restarts) + SWR/React Query on the client; in-memory dict here has the same architectural shape so it's a clean swap to Redis later when multi-worker / quota pressure arrives.

Prerequisite refactor: extract the bodies of get_slack_messages, get_gmail_messages, get_github_activity into pure async functions in backend/app/services/feed_fetchers.py so the cache and (future) background poller can both call them.

Files

  • NEW backend/app/services/signal_feed_cache.py (~50 lines)
  • NEW backend/app/services/feed_fetchers.py (extracted from gateway.py)
  • MOD backend/app/routers/gateway.py — handlers become cache-aside wrappers; DELETE /<service>/disconnect clears that user's cache entry
  • NEW backend/tests/test_signal_feed_cache.py — TTL behavior, expiry, clear

Acceptance

  • First load after backend restart matches today's latency (~2–5s)
  • Subsequent loads <100ms (verify via DevTools Network panel)
  • Disconnect + reconnect a service shows fresh data, not stale-cached
  • pytest clean; new cache tests pass

Out of scope

  • Redis migration (for multi-worker / quota pressure later)
  • Client-side localStorage cache (server cache already <100ms; revisit if true 0ms first-paint becomes a goal)

Related

Sub-issue: auto-refresh the cache every 120s so the feed stays current without manual reload. Filed separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions