skip to content

In an Angular SSR app, why don't HttpClient GET requests made during the server render run again in the browser?

level: juniorimportance: must knowfreq 58%

answer

  1. server work reused in the browser
  2. serialized into the HTML
  3. on by default with hydration
  4. ends once the app is stable

basics

~20 s

Angular's HTTP transfer cache records eligible HttpClient responses made during the server render, serializes them into the HTML, and replays them in the browser during hydration, so the same GET or HEAD request is not sent twice.

solid answer

~50 s

Without it, server-side rendering would fetch the same data twice: once on the server to build the HTML and again when the browser app boots. `provideClientHydration()` turns the **HTTP transfer cache** on by default. On the server a built-in interceptor stores each eligible `HttpClient` response (by default `GET` and `HEAD` requests that carry no auth headers, cookies or credentials and are not marked uncacheable) in `TransferState`, which Angular serializes into a `<script type="application/json">` tag in the page. In the browser the same interceptor looks up a matching entry by a key built from the method, URL, params, body and response type, and returns it instead of calling the network. Once the application becomes stable in the browser the cache switches off, so later requests go to the network as usual. `withNoHttpTransferCache()` turns it off entirely.

code

ts · 18 lines
ts
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';

interface Headline { id: number; title: string; }

@Component({
  selector: 'app-headlines',
  template: `
    @for (h of headlines(); track h.id) {
      <h2>{{ h.title }}</h2>
    }
  `,
})
export class Headlines {
  private http = inject(HttpClient);
  headlines = toSignal(this.http.get<Headline[]>('/api/headlines'), { initialValue: [] });
}

go deeper

for a junior

Recall that provideClientHydration() turns on the transfer cache, that it covers HttpClient GET and HEAD requests, and that it stops the browser repeating the server's requests.

for a middle

Explain the flow: server interceptor, TransferState, the JSON script tag, key matching in the browser, and the switch-off at application stability.

for a senior

Show you know the limits: key mismatches, auth and cookie exclusions, and requests outside HttpClient all bring the double fetch back, and you can prove which one from the page source.

for a principal

Frame it as a contract between server and browser code paths: requests must be identical on both sides, and what is embedded in HTML must be safe for anyone who can read that HTML.

## The double-fetch problem With **server-side rendering (SSR)**, Angular runs your application on the server, lets components call their services, waits for the data and produces finished HTML. The browser then downloads the same JavaScript and boots the same application to make that HTML interactive (**hydration**). Without extra help, the `HttpClient` calls components made on the server would normally run a second time in the browser: the API receives twice the traffic, and the page can flicker while the browser-side request is in flight. Angular's answer is the **HTTP transfer cache**: responses fetched on the server travel inside the HTML, and the browser replays them instead of asking the network again. ## How it works, step by step 1. On the server, a root `HttpClient` interceptor that Angular registers for you sees each outgoing request. 2. If the request is eligible, the interceptor lets it run, then writes the response body, status, status text, URL and response type into `TransferState` under a hashed key made from the method, response type, URL, serialized body and sorted query params. 3. Before the HTML is sent, `TransferState` is serialized as JSON into a `<script type="application/json">` tag. Its id is the app id plus `-state`, so `ng-state` with the default app id. 4. In the browser, `TransferState` reads that tag lazily the first time it is injected. 5. When a component issues the same request during bootstrap, the interceptor builds the same key, finds the entry and returns an `HttpResponse` built from it without touching the network. 6. When the application becomes **stable** in the browser (no pending work left), the cache is deactivated. Every request after that goes to the network. ## What is eligible by default - Only `GET` and `HEAD` requests. - Not requests carrying `Authorization`, `Proxy-Authorization` or `Cookie` headers. - Not requests sent with `withCredentials` or with the Fetch `credentials` mode set to `include` or `same-origin`. - Not requests or responses whose `Cache-Control` says `no-store`, `no-cache` or `private`, and not responses carrying `Set-Cookie`. - **No response headers** are transferred unless you list them with `includeHeaders`. Those rules, and the options that relax them, are configured with `withHttpTransferCacheOptions()`. ## Turning it on and off | Configuration | Effect | |---|---| | `provideClientHydration()` | Hydration plus the transfer cache with default options | | `provideClientHydration(withHttpTransferCacheOptions({...}))` | Transfer cache with your options | | `provideClientHydration(withNoHttpTransferCache())` | Hydration without the transfer cache | | `http.get(url, { transferCache: false })` | Skip the cache for one request | Passing both `withNoHttpTransferCache()` and `withHttpTransferCacheOptions()` is a contradiction, and Angular throws a configuration error in development mode. ```ts // app.config.ts, shared by the browser and server builds import { ApplicationConfig } from '@angular/core'; import { provideClientHydration } from '@angular/platform-browser'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [provideHttpClient(), provideClientHydration()], }; ``` ## What it does not do - It is **not** a general HTTP cache. It does not honour `max-age`, and it stops being consulted once the app is stable. - It only sees requests that go through `HttpClient`, including `httpResource()`, which is built on it. A native `fetch()` call or a third-party SDK with its own transport is invisible to it; carry that data yourself with `TransferState`. - It helps only if the browser makes the **same** request as the server. A different origin, an extra query param or a different body produces a different key and a miss. - Entries are not deleted when they are read, so two identical requests made before stability are both answered from the cache. ## How it relates to TransferState The transfer cache is a specialised client of `TransferState`, the general key-value store Angular serializes into the page: - The transfer cache fills `TransferState` automatically, for `HttpClient` traffic only, under hashed keys you never see. - You use `TransferState` with `makeStateKey()` yourself for data that did not come through `HttpClient`. - Both end up in the same JSON script tag, so both add to the weight of the HTML and both are readable by anyone who receives the page. Knowing the split helps you answer the natural follow-up: "what if the data comes from somewhere else?" ## Why interviewers ask it The question checks whether a candidate understands that SSR and the client app are the same code running twice, and that Angular closes the data gap automatically for `HttpClient`. A good answer names `provideClientHydration()` as the switch, the eligibility defaults, and the cut-off at application stability.

  • Where does the cached data sit in the page the server sends?
    In a `<script type="application/json">` tag appended to the end of `<body>`, with the id `ng-state` for the default app id. It holds the JSON of `TransferState`, with `<` and `/` escaped so the data cannot break out of the tag. If the store is empty, Angular omits the tag. Anyone can read it in the page source.
  • How do you stop one request from being cached while keeping the feature on?
    Pass `transferCache: false` in that request's options, for example `http.get('/api/quote', { transferCache: false })`. To exclude a whole family of URLs, use the `filter` callback in `withHttpTransferCacheOptions()`. To turn the feature off for the whole app, add `withNoHttpTransferCache()` to `provideClientHydration()`.

saying these in an interview costs you the question

  • The browser always refetches everything; SSR only speeds up the first paint.
  • You must write your own interceptor to reuse server responses.
  • Every HttpClient request is cached, including POSTs and authenticated calls.
  • The cached responses keep being served for the whole browser session.
  • Plain fetch() calls in components are cached the same way.