Why does Remix.run trigger excessive API requests in route loaders?

I’m having trouble with my Remix app making way too many API calls to an external service. I set up a loader function in my main route that calls a restaurant review API, but something weird is happening in production.

I thought I was being smart by caching the API response in a session cookie that expires after 1 hour. The idea was simple - if the data exists in the session, use that instead of making another API call. This seemed to work fine during development.

But when I deployed to production, my API usage went crazy. I’m seeing over 300 API calls per day, even on days when my analytics show zero visitors. That makes no sense to me.

Here’s the basic structure of my loader:

export async function loader({ request }: LoaderFunctionArgs) {
  const userSession = await getUserSession(request.headers.get('Cookie'));

  let restaurantData = null;
  let consent = null;
  let notification = null;
  let errorMsg = null;

  if(userSession.has("consent")){
    consent = userSession.get("consent");
  }

  if(userSession.has("restaurant")){
    restaurantData = JSON.parse(userSession.get("restaurant") as string);
  } else {
    const requestOptions = {
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Authorization': `Bearer ${process.env.RESTAURANT_API_KEY}`,
        'Content-Type': 'application/json'
      }
    };

    const response = await fetch('https://api.restaurant-reviews.com/v2/locations/123', requestOptions);
    restaurantData = await response.json();
    userSession.set("restaurant", JSON.stringify(restaurantData));
  }

  if (userSession.has("notification")) {
    notification = userSession.get("notification");
  }
  if (userSession.has("errorMsg")) {
    errorMsg = userSession.get("errorMsg");
  }

  return json({ notification, errorMsg, restaurantData, consent }, {
    headers: {
      'Set-Cookie': await saveSession(userSession),
    },
  });
}

My session setup looks like this:

import { createCookieSessionStorage } from "@remix-run/node";

const { getUserSession, saveSession, clearSession } =
  createCookieSessionStorage({
    cookie: {
      name: "__app_session",
      httpOnly: true,
      maxAge: 3600, // 1 hour
      path: "/",
      sameSite: "lax",
      secrets: ["my-secret-key"],
      secure: true,
    },
  });

export { getUserSession, saveSession, clearSession };

Has anyone experienced this before? I had to remove the API calls completely because I can’t figure out what’s causing this behavior. It makes me worried about using Remix in production if I can’t control when loaders execute.

Classic session storage issue - I’ve been there. Your cookie expires after an hour, but you’re not actually checking if the data expired before using it. The session cookie keeps the data around, but once it expires, Remix spins up a fresh empty session on every request. That’s why userSession.has("restaurant") always returns false for new sessions.

Real issue though? You’re setting the cookie on every single response whether you hit the API or not. That’s a ton of unnecessary session writes. Plus if your session storage craps out (corrupted data, size limits, whatever), it’ll silently fail and your cache check becomes useless.

I’d add a timestamp to your cached data and actually verify it’s still good. Better yet, use proper caching like Redis or at least throw in some logging so you can see when and why these API calls are firing. Those calls with zero visitors? Probably bots or health checks hammering your routes.

You’re probably missing the session commit check. I had the same weird issue - Remix was creating new sessions for every bot hit or prefetch. Only call saveSession when you actually change session data, not on every response. Also check if your host has health checks or monitoring constantly pinging your routes - that’d explain those phantom API calls with zero real traffic.

Hit the same issue with a different framework - exact same pattern. Your caching logic isn’t the problem. You’re calling saveSession() on every request whether the session changed or not. That forces a new cookie each time, making browsers and crawlers treat every response as a cache miss.

Also, production usually has multiple server instances or edge functions spinning up and down. Each cold start creates a new execution context. If your session data gets corrupted during transmission, the has() check fails silently and triggers fresh API calls.

Add some debug logging around your session checks and API calls, then watch your server logs for a few days. You’ll probably find the extra calls are from bots - uptime monitors, search crawlers, security scanners that don’t maintain session state properly.