# Performance

This guide covers integration patterns that keep Activity Feeds fast as your traffic grows. For the platform architecture behind these recommendations, see [Architecture & Benchmark](/activity-feeds/docs/php/architecture/).

## Read shared feeds server-side; update from real-time events

For feeds many users read the same way (global timelines, editorial feeds, live events, leaderboards), route the initial read through your backend instead of calling the API directly from every client SDK. Many clients loading the same feed means many identical reads. A single server-side read path lets you cache that result once (and is required for [hot feed cache](/activity-feeds/docs/php/hot-feed-cache/) to apply), batch, and apply access control in one place.

Once loaded, prefer real-time feed events over re-reading: let the client SDK apply activity and reaction updates incrementally rather than polling. Reads and real-time events are complementary. The read establishes the baseline; events keep it fresh. Pass `watch: true` when [reading the feed](/activity-feeds/docs/php/feeds/#reading-a-feed), then subscribe to feed events on the client. This mainly cuts re-reads within a session; it doesn't remove the initial shared load, which is why the read still belongs on your backend for high-fanout surfaces.

## Choose the right feed pattern

| Pattern            | Best for                         | Performance note                                                               |
| ------------------ | -------------------------------- | ------------------------------------------------------------------------------ |
| Flat / recency     | Per-user timelines               | Fast by default with materialized feeds                                        |
| Aggregation        | Grouped notification-style reads | Aggregation keys are precomputed on write                                      |
| Expression ranking | Engagement-driven ordering       | Dynamic; see [custom ranking](/activity-feeds/docs/php/custom-ranking/) |
| Interest ranking   | Personalized for-you feeds       | Per-user ordering at read time                                                 |

Use [For you feed](/activity-feeds/docs/php/for-you-feed/) when every reader needs different ordering.

## Prefer materialized selectors for hot read paths

[Activity selectors](/activity-feeds/docs/php/activity-selectors/) fall into two groups with very different cost profiles:

- **`following` and `current_feed`** read from materialized feeds. The feed is maintained as activities are written, so a read is essentially a pre-computed lookup. Cost scales with read volume, not with how much data exists. Use these for your highest-traffic surfaces.
- **`popular`, `interest`, `query`, and `proximity`** are computed at read time. They're powerful for discovery and personalization, but each read does real query work, so cost grows with your dataset and query complexity. Use them where their capability is needed, not as the default for every feed.

The `current_feed` portion of a read is served from the materialized feed when:

- The feed uses the default most-recent-first ordering. A custom sort (for example oldest-first) falls back to read-time computation so the ordering can be applied correctly.
- Filters are limited to simple tag and id filters: `filter_tags`, `id` with `$in`, `expires_at` with `$exists` / `$lte` / `$lt`, and `$and` / `$or` combinations of those. Any other field or operator (ranges, other comparisons, arbitrary custom-field predicates) falls back to read-time computation.

When any of these conditions isn't met, `current_feed` still returns correct results. It just computes them at read time instead of serving the pre-computed feed.

When you combine a materialized selector with a computed one (for example `popular`), the two are sourced in parallel and merged: the materialized portion keeps its fast path, and only the added computed selectors cost read-time work.

- **`following`** is always served from the materialized feed when combined with computed selectors. There are no extra conditions, because a following feed is inherently materialized.
- **`current_feed`** gets the same parallel treatment on a ranked feed, but only when it appears once (no second `current_feed` selector) with the default sort and simple filters described above. If those conditions aren't met (a non-ranked feed, more than one `current_feed` selector, or a non-default sort/filter), the `current_feed` portion computes at read time instead. (The sort and filter conditions apply to `current_feed` only; they do not affect `following`.)

When a feed includes both `following` and `current_feed` selectors, the read uses the `following` path for parallel sourcing.

## Hot feed cache

For shared ranked feeds at very high read volume, Stream can enable [hot feed cache](/activity-feeds/docs/php/hot-feed-cache/) per app and feed. It's an operational opt-in ([contact support](https://getstream.io/contact/support/)), not a feed pattern you configure in your feed group. Use it when every reader should see the same content and ordering.

Unfiltered reads are cached by default once enabled. Reads with a request-level filter are not, except `filter_tags` when Stream has opted that feed in for tag caching. Request tag caching as part of enablement if you need it.

## Minimize enrichment on read

Each enrichment option adds work at read time. Request only the fields your UI needs. See [Enrichment options](/activity-feeds/docs/php/feeds/#enrichment-options) on the Feeds page.

Comments and reactions enrichment fan out to additional lookups, so they're the most expensive to leave on by default.

When hot feed cache is active, `own_reactions` is omitted from cached responses (they're per-user and can't be shared). If your UI needs them, fetch them for the whole page in a single call with [batch query activity reactions](/activity-feeds/docs/php/reactions/#batch-query-activity-reactions) rather than one request per activity.

## Paginate efficiently

Use reasonable page sizes and avoid re-requesting the first page on every app open when a cached copy on your server is enough.

Ranked feeds use cursor-backed pagination. The token stays valid as long as the ranking configuration is unchanged. Updating the feed group's ranking, changing [filters](/activity-feeds/docs/php/feeds/#filters), or changing `external_ranking` parameters mid-pagination invalidates outstanding tokens, so expect the next page to restart rather than continue.

When [hot feed cache](/activity-feeds/docs/php/hot-feed-cache/) is enabled, keep `external_ranking` identical on every page of a paginated read. Change it mid-pagination and the API rejects the stale token with a `400` (`pagination token expired, please refresh the feed`); drop it entirely and the request falls off the cached path and is recomputed. Either way you lose the cached snapshot for that page. See [Pagination](/activity-feeds/docs/php/hot-feed-cache/#pagination) and [External ranking](/activity-feeds/docs/php/hot-feed-cache/#external-ranking) on the hot feed cache page.

## Avoid server-side polling

Use [webhooks](/activity-feeds/docs/php/events/) instead of polling feeds on a fixed interval from background jobs. Cron-driven read endpoints are a common source of unnecessary load when no user is actively viewing the feed.

## Design feed groups for your read pattern

- Use [filters](/activity-feeds/docs/php/feeds/#filters) for small, stable tag sets on the general read path. Simple tag filters can be served directly from materialized feeds; complex or high-cardinality filter expressions force read-time computation and give up that fast path. This is separate from [hot feed cache](/activity-feeds/docs/php/hot-feed-cache/): filtered reads are not hot-cached unless Stream has enabled `filter_tags` caching for that feed. Filters are not a substitute for search.
- Ranked-feed pagination tokens are tied to the ranking configuration. See [Paginate efficiently](#paginate-efficiently) for how feed-group changes affect in-flight cursors.


---

This page was last updated at 2026-07-03T16:22:09.581Z.

For the most recent version of this documentation, visit [https://getstream.io/activity-feeds/docs/php/performance/](https://getstream.io/activity-feeds/docs/php/performance/).