# Hot Feed Cache

Hot feed cache speeds up high-traffic feeds that many users read the same way. Stream computes the feed once, caches the result for a short window, and serves that shared ordering to all readers. Thousands of duplicate per-user reads become one cached read.

<admonition type="info">

Hot feed cache is off by default. [Contact support](https://getstream.io/contact/support/) to request access.

</admonition>

## When to use it

Use hot feed cache for feeds where all readers should see the same content and ordering:

- Global or editorial timelines
- Live event feeds
- Leaderboards
- Any ranked feed with identical results for every user

Do **not** use it for per-user personalized ordering. Feeds with [interest ranking](/activity-feeds/docs/php/custom-ranking/) or other per-user rank signals are ineligible and fall back to a normal read.

## How it works

- **Refresh window**: The cache refreshes on an interval on the order of tens of seconds to about a minute. Content can be that stale between refreshes; decide whether that fits your use case (a leaderboard is usually fine; a sub-second live ticker might not).
- **Shared ordering**: All readers hitting the same cache entry see the same activity order.
- **Frozen randomness and time**: If your ranking expression uses randomness ([`rand_normal`](/activity-feeds/docs/php/custom-ranking/#functions)) or time-based values ([`current_time`](/activity-feeds/docs/php/custom-ranking/#constants)), the result is frozen per cache snapshot (shared across readers, re-computed each refresh) rather than evaluated per request.
- **Transparent fallback**: If a request is not eligible for the cache, Stream serves a normal feed read with no error. (Long-idle pagination tokens can expire; see [Pagination](#pagination).) No client-side flag is required once Stream has enabled the feature for your feed.

## Eligibility

Hot feed cache applies when **all** of the following are true:

- Stream has enabled the feature for your app and feed
- The read is made **server-side** (backend SDK or server token), not from a client SDK
- The feed uses default (recency) or [expression-based ranking](/activity-feeds/docs/php/custom-ranking/), including [external ranking](/activity-feeds/docs/php/custom-ranking/#providing-external-data-when-reading-the-feed)
- The read does not pass a request-level filter, unless Stream has enabled `filter_tags` caching for that feed (see below)

Reads that pass a request-level filter are not cached. The one exception is `filter_tags`, which can be cached only when Stream has enabled tag caching for that feed. Request it as part of your hot feed cache setup. Each distinct tag set is cached separately, up to a per-feed limit Stream configures.

## Requests that aren't cached

When a request is not cacheable, Stream falls back to a normal read automatically. No error is returned.

| Condition                                                       | Behavior    |
| --------------------------------------------------------------- | ----------- |
| Client-side request                                             | Normal read |
| Interest or personalized ranking                                | Normal read |
| Interest weights in ranking                                     | Normal read |
| Ranking expressions using per-user state (`is_read`, `is_seen`) | Normal read |
| Request includes a filter other than `filter_tags`              | Normal read |
| `filter_tags` on a feed not opted in for tag caching            | Normal read |

## Response differences when served from cache

When a request **is** served from cache, the response differs in a few ways:

- **`own_reactions` omitted**: Cached pages are identical across users, so per-user `own_reactions` are not included. 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. To hydrate reactions on comment threads the same way, use [batch query comment reactions](/activity-feeds/docs/php/reactions/#batch-query-comment-reactions).
- **No blocked-user filtering**: Block lists are not applied to cached responses. Apply them in your backend before returning data to clients if required.
- **`score_vars` omitted on cached pages**: If you set `include_score_vars` in [enrichment options](/activity-feeds/docs/php/feeds/#enrichment-options), cached pages omit `score_vars`; they're returned only on a freshly computed page. See [Inspecting ranking variables](/activity-feeds/docs/php/custom-ranking/#inspecting-ranking-variables).

## Pagination

Paginated reads are served from an immutable snapshot of the ranked order, so every page of a given pagination stays consistent and a routine content refresh does not interrupt an in-progress pagination. The snapshot is kept alive while you're actively paging (each page you fetch extends it), so the pagination token expires only after a prolonged pause mid-pagination. If it does expire, the API returns `400` with `pagination token expired, please refresh the feed`. Restart from page 1.

Changing `external_ranking` mid-pagination also rejects the stale token with that error. Dropping `external_ranking` on a follow-up page falls off the cached path and is recomputed instead. See [External ranking](#external-ranking) and [Paginate efficiently](/activity-feeds/docs/php/performance/#paginate-efficiently) for how ranked-feed cursors work in general.

## External ranking

External ranking is supported. Ordering is shared per unique `external_ranking` parameter set, so different parameter values produce separate cache entries.

<admonition type="warning">

**Performance:** Keep `external_ranking` combinations bounded. When you use a ranking view with `external_ranking`, the ranked result is cached per distinct combination of `external_ranking` values. Reusing a bounded set of combinations (typically dozens to a few hundred) lets reads be served from cache and stay fast. Sending a unique `external_ranking` payload on every request defeats this cache and re-runs ranking on each read, which increases latency under high request rates. Prefer a stable, limited set of ranking-parameter combinations, and remember to resend the same `external_ranking` on paginated requests.

</admonition>

**Resend `external_ranking` on every page.** Keep the payload identical on every page of a paginated read. Change `external_ranking` mid-pagination and the API rejects the stale token with `400` (`pagination token expired, please refresh the feed`). Drop it entirely and the request falls off the cached path and is recomputed on the canonical read path. Either way you lose the cached snapshot for that page.

<Tabs>

```js label="Node"
// Server-side call (backend SDK / server token)
// Page 1
const page1 = await feed.getOrCreate({
  user_id: "system",
  limit: 20,
  external_ranking: {
    popularity_multiplier: 1,
    share_multiplier: 100,
  },
});

// Page 2: pass the same external_ranking
const page2 = await feed.getOrCreate({
  user_id: "system",
  limit: 20,
  next: page1.next,
  external_ranking: {
    popularity_multiplier: 1,
    share_multiplier: 100,
  },
});
```

```go label="Go"
// Server-side call (backend SDK / server token)
params := map[string]any{
  "popularity_multiplier": 1,
  "share_multiplier":      100,
}

page1, err := feed.GetOrCreate(ctx, &getstream.GetOrCreateFeedRequest{
  UserID:          getstream.PtrTo("system"),
  Limit:           getstream.PtrTo(20),
  ExternalRanking: params,
})

page2, err := feed.GetOrCreate(ctx, &getstream.GetOrCreateFeedRequest{
  UserID:          getstream.PtrTo("system"),
  Limit:           getstream.PtrTo(20),
  Next:            page1.Data.Next,
  ExternalRanking: params,
})
```

```python label="Python"
# Server-side call (backend SDK / server token)
params = {"popularity_multiplier": 1, "share_multiplier": 100}

page1 = feed.get_or_create(
    user_id="system",
    limit=20,
    external_ranking=params,
)

page2 = feed.get_or_create(
    user_id="system",
    limit=20,
    next=page1.next,
    external_ranking=params,
)
```

```php label="php"
// Server-side call (backend SDK / server token)
$params = (object) [
    'popularity_multiplier' => 1,
    'share_multiplier' => 100,
];

$page1 = $feed->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(
        userID: 'system',
        limit: 20,
        externalRanking: $params,
    )
);

// Page 2: pass the same externalRanking
$page2 = $feed->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(
        userID: 'system',
        limit: 20,
        next: $page1->getData()->next,
        externalRanking: $params,
    )
);
```

```java label="Java"
// Server-side call (backend SDK / server token)
Map<String, Object> params = new HashMap<>();
params.put("popularity_multiplier", 1);
params.put("share_multiplier", 100);

GetOrCreateFeedResponse page1 = feed.getOrCreate(
    GetOrCreateFeedRequest.builder()
        .userID("system")
        .limit(20)
        .externalRanking(params)
        .build()
).getData();

// Page 2: pass the same externalRanking
GetOrCreateFeedResponse page2 = feed.getOrCreate(
    GetOrCreateFeedRequest.builder()
        .userID("system")
        .limit(20)
        .next(page1.getNext())
        .externalRanking(params)
        .build()
).getData();
```

```csharp label="C#"
// Server-side call (backend SDK / server token)
var externalRanking = new Dictionary<string, object>
{
    ["popularity_multiplier"] = 1,
    ["share_multiplier"] = 100,
};

var page1 = await feed.GetOrCreateFeedAsync(
    new GetOrCreateFeedRequest
    {
        UserID = "system",
        Limit = 20,
        ExternalRanking = externalRanking,
    }
);

// Page 2: pass the same ExternalRanking
var page2 = await feed.GetOrCreateFeedAsync(
    new GetOrCreateFeedRequest
    {
        UserID = "system",
        Limit = 20,
        Next = page1.Next,
        ExternalRanking = externalRanking,
    }
);
```

```ruby label="Ruby"
# Server-side call (backend SDK / server token)
params = {
  'popularity_multiplier' => 1,
  'share_multiplier' => 100,
}

page1 = feed.get_or_create_feed(
  GetStream::Generated::Models::GetOrCreateFeedRequest.new(
    user_id: 'system',
    limit: 20,
    external_ranking: params,
  )
)

# Page 2: pass the same external_ranking
page2 = feed.get_or_create_feed(
  GetStream::Generated::Models::GetOrCreateFeedRequest.new(
    user_id: 'system',
    limit: 20,
    next: page1.next,
    external_ranking: params,
  )
)
```

</Tabs>

## Requesting access

Contact [support](https://getstream.io/contact/support/) with:

- Your Stream app ID
- The feed group and feed ID (or feed pattern) you want optimized
- Whether you need `filter_tags` caching (and which tag sets you expect to read at high volume)

Stream enables hot feed cache per app and feed after reviewing your use case.


---

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

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