Why Does My Angular HTTP Call Fire Twice? Causes and Fixes [2026]

Link copied
Why Does My Angular HTTP Call Fire Twice? Causes and Fixes [2026]

Why Does My Angular HTTP Call Fire Twice? Causes and Fixes [2026]

You open the Network panel to check one request and see two identical GET /api/orders lines. Sometimes it's harmless; often it doubles your API load, causes a flash of old data, or creates two records when the request was a POST. Duplicate requests in Angular almost never come from Angular "rendering twice". They come from a handful of patterns, and the Network panel tells you which one you have if you know what to look for.

This is lesson 10.4 of the Angular Tutorial. Lesson 10.3 covered components that update too little; this one covers requests that happen too often. You'll learn why HttpClient Observables are cold, the common ways one request becomes two, how SSR and the HTTP transfer cache fit in, and a Network-panel routine to diagnose duplicates in minutes.

The one fact behind most duplicates #

HttpClient methods return cold Observables. Nothing is sent when you call http.get(); a request is sent each time something subscribes:

const orders$ = this.http.get<Order[]>('/api/orders');   // no request yet

orders$.subscribe(render);     // request 1
orders$.subscribe(updateBadge); // request 2

That's useful (retrying is just resubscribing), but it means every extra subscriber is an extra request. Keep that in mind for the causes below.

Cause Network panel signature
Multiple subscribers to one cold Observable Two identical requests, same initiator area, a few milliseconds apart
An effect or computed chain re-triggering a load Repeats whenever an unrelated signal changes
SSR plus a client request the transfer cache didn't cover One request in the server logs, one in the browser
retry or polling logic Requests spaced by your retry delay or interval
CORS preflight An OPTIONS request followed by the real one
A component created twice Two requests, each from a separate component instance

Cause 1: Two subscribers to the same Observable #

The most common version is using the async pipe twice on the same Observable:

<!-- ✗ two subscriptions → two requests -->
@if (orders$ | async; as orders) {
  <h2>{{ (orders$ | async)?.length }} orders</h2>
  <app-order-table [orders]="orders" />
}

Other versions: a service method that returns http.get() and is called from both the component and a child; or a subscribe() in the component plus an async pipe in the template.

Fix A: subscribe once. Use the as alias everywhere in that block ({{ orders.length }}), or convert to a signal so the template reads a value instead of subscribing:

orders = toSignal(this.http.get<Order[]>('/api/orders'), { initialValue: [] });
<h2>{{ orders().length }} orders</h2>
<app-order-table [orders]="orders()" />

Fix B: use a resource. httpResource() makes one request per change of its reactive URL and exposes the result as signals, however many places read it:

orders = httpResource<Order[]>(() => '/api/orders');

Fix C: share deliberately. When several unrelated consumers really need the same stream, make it multicast with shareReplay:

@Injectable({ providedIn: 'root' })
export class CatalogService {
  private http = inject(HttpClient);
  readonly categories$ = this.http.get<Category[]>('/api/categories').pipe(
    shareReplay({ bufferSize: 1, refCount: true }),
  );
}

refCount: true drops the shared subscription when the last subscriber leaves, so the next subscriber triggers a fresh request. With refCount: false, the result is cached for the lifetime of the service, which is what you want for reference data and not what you want for anything that changes. Store the shared Observable in a field; a method that builds a new pipe(shareReplay(...)) on every call shares nothing.

Cause 2: Effects and reactive loads that re-run #

Signals make it easy to write a load that re-runs more often than you think:

// ✗ reads filters() AND user(); a profile update reloads the orders
constructor() {
  effect(() => {
    const f = this.filters();
    const userId = this.user().id;
    this.http.get<Order[]>('/api/orders', { params: { ...f, userId } })
      .subscribe(o => this.orders.set(o));
  });
}

Every signal read inside the effect is a dependency. If user() is replaced with a new object when the user changes their avatar, the effect runs again and fetches orders again. Previous subscriptions aren't cancelled either, so responses can arrive out of order (lesson 10.6).

Fix: depend only on what the request needs, and let a resource handle cancellation:

userId = computed(() => this.user().id);   // only changes when the id changes

orders = httpResource<Order[]>(() => ({
  url: '/api/orders',
  params: { ...this.filters(), userId: this.userId() },
}));

A computed() that returns a primitive (the id) only notifies when that primitive changes, so profile edits no longer trigger a reload. If you must keep an effect, wrap reads that shouldn't be dependencies in untracked().

Cause 3: SSR without the transfer cache #

With server-side rendering, the server runs your components and makes their HTTP calls to render HTML. Then the browser boots the same components, which make the same calls again, unless the response is reused.

Angular's HTTP transfer cache does that reuse. When hydration is enabled with provideClientHydration(), it's on by default: responses fetched with HttpClient on the server are embedded in the page and replayed on the client instead of re-requested. It deliberately skips some requests:

Not cached by default Why
Methods other than GET and HEAD Not safe to replay; includePostRequests opts in for idempotent POSTs
Requests with Authorization, Proxy-Authorization or Cookie headers Could leak user data into cached HTML; includeRequestsWithAuthHeaders opts in
Requests sent with credentials Same reason; includeRequestsWithCredentials opts in
Cache-Control: no-store, no-cache or private The server asked not to cache
Calls made with fetch() directly The cache only sees requests made through HttpClient

So if an authenticated API call fires on both server and client, that's the default behaviour, not a bug. Decide per endpoint: opt in with withHttpTransferCacheOptions() if it's safe to embed the response in the HTML, skip the call on the server, or accept the duplicate. Lesson 9.6 covers the transfer cache and TransferState in detail.

provideClientHydration(
  withHttpTransferCacheOptions({
    includePostRequests: true,                       // only for idempotent POST endpoints
    filter: req => !req.url.includes('/api/cart'),   // never embed the cart
  }),
),

To tell whether you have this cause, check where each request comes from: one in your server logs (or the SSR process output) and one in the browser's Network panel means the transfer cache didn't apply.

Cause 4: Retry, refresh and polling loops #

Retries look like duplicates. retry(2) on a failing request sends up to three requests, and an interceptor that retries on 401 after refreshing a token sends the original request twice by design. Problems start when both a global interceptor and a service add retries (multiplying them), or when a refresh loop has no stop condition. Check for retry, repeat, interval and timer in the chain and in your interceptors. Lesson 6.4 shows retry patterns that back off and stop.

Myths worth ruling out #

  • "Dev mode renders everything twice." Angular doesn't mount components twice in development. The development-only verification pass after change detection re-evaluates template expressions, not constructors or ngOnInit. But if a template calls a method that makes an HTTP request, that method runs on every check, including the verification pass. Never start requests from template expressions.
  • "It's the OPTIONS request." A cross-origin request with custom headers triggers a CORS preflight: an OPTIONS request, then the real one. That's the browser, not Angular. Filter by method in the Network panel; if the "duplicate" is an OPTIONS, it's expected.
  • "The component is created twice." Occasionally true: the same component in a layout and in a routed page, an @if that toggles during load, or a route that's re-created on navigation. A console.count('OrdersPage') in the constructor settles it quickly.

Diagnosing in the Network panel #

  1. Filter to Fetch/XHR, tick Preserve log, and reload.
  2. Compare the two requests. Same URL and parameters, or slightly different? Different parameters usually mean a reactive load re-ran (cause 2).
  3. Check the method. OPTIONS + GET is a preflight.
  4. Open the Initiator column (or hover it). It shows the call stack that sent each request. Two different stacks (an async pipe and a subscribe(), or two components) point to cause 1 or a duplicated component.
  5. Look at the timing. A few milliseconds apart: two subscribers. Spaced by a fixed delay: retry or polling. Only on first load with SSR: transfer cache.
  6. Count instances with console.count() in the constructor if the initiators are different components of the same type.

Gotchas #

Symptom Cause Fix
Two identical requests on page load async pipe used twice on one Observable Single subscription with as, toSignal() or httpResource()
Request repeats when unrelated data changes Effect reads more signals than the request needs Narrow with computed() or use httpResource(); untracked() for incidental reads
shareReplay doesn't help A method builds a new shared Observable on every call Store the shared Observable in a field
Authenticated call fires on server and client Transfer cache skips requests with Authorization headers Opt in only if safe, or skip the call on the server
Duplicate POST creates two records Save button double-clicked or two subscribers to a mutation exhaustMap for the click (lesson 10.6); subscribe once
Two requests, the first is OPTIONS CORS preflight Expected; reduce with same-origin APIs or a proxy

What's next #

Lesson 10.5 moves from requests that happen too often to subscriptions and effects that live too long. You'll audit a codebase for leaks: retrofitting takeUntilDestroyed, replacing manual subscriptions with toSignal, adding effect cleanup, and using lint rules and searches to keep them out.

Try it yourself #

Open the Network panel with Preserve log on and load your three busiest pages. For every request that appears twice, open its Initiator stack and classify it with the table at the top of this lesson. Fix the most common cause first.

YouOn our SSR product page, GET /api/products/42 shows up in the browser Network panel even though the server already rendered it. GET /api/reviews/42 doesn’t. Both use HttpClient. Why the difference?
Claude · used search_documentationThe HTTP transfer cache only replays GET and HEAD requests that carry no Authorization, Proxy-Authorization or Cookie header. Your authInterceptor adds an Authorization header to every request under /api/products (for member pricing), so that response isn’t embedded in the HTML and the client fetches it again. /api/reviews is public and gets replayed from the cache. You have three options: (1) leave it, since member prices are user-specific and shouldn’t be embedded in shared or cached HTML; (2) split the endpoint into a public product request (cached) and a small authenticated price request (client only); or (3) set includeRequestsWithAuthHeaders: true in withHttpTransferCacheOptions(), but only if the rendered HTML is never cached by your CDN. Option 2 is usually the right one.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied Angular When Angular is launched ? February 8, 2024 · 1 min read When was Angular launched? # Angu…

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

1 comment

1 thought on “Why Does My Angular HTTP Call Fire Twice? Causes and Fixes [2026]”

  1. Pingback: Find Angular Subscription and Effect Leaks [2026]

Leave a Comment

Your email stays private. Required fields are marked *

1 thought on “Why Does My Angular HTTP Call Fire Twice? Causes and Fixes [2026]”

  1. Pingback: Find Angular Subscription and Effect Leaks [2026]

Leave a Comment

Your email stays private. Required fields are marked *