The excessive API calls you’re experiencing are likely caused by Remix’s aggressive prefetching behavior combined with how your session storage is being handled. This is a common production gotcha that catches many developers off guard!
Understanding the Root Cause
Remix automatically prefetches routes when users hover over links or when certain conditions are met, which means your loader functions can execute far more frequently than actual page visits. In production environments with CDNs, monitoring tools, and bot traffic, this effect gets amplified significantly.
Here’s what’s happening in your code:
- Session Creation on Every Request: Even when
userSession.has("restaurant") returns true, you’re still calling saveSession(userSession) in the response headers
- Prefetch Triggers: Route prefetching can cause your loader to run without actual user navigation
- Session Expiration Edge Cases: The 1-hour
maxAge might not align perfectly with when your session checks occur
The Complete Solution
Here’s your improved loader with proper caching logic:
javascript
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;
let shouldUpdateSession = false;
// Check for existing consent
if(userSession.has(“consent”)){
consent = userSession.get(“consent”);
}
// Smart restaurant data caching
if(userSession.has(“restaurant”) && userSession.has(“restaurant_timestamp”)){
const timestamp = userSession.get(“restaurant_timestamp”);
const oneHourAgo = Date.now() - (60 * 60 * 1000);
if (timestamp > oneHourAgo) {
// Data is still fresh, use cached version
restaurantData = JSON.parse(userSession.get("restaurant") as string);
} else {
// Data is stale, fetch new data
shouldUpdateSession = true;
}
} else {
// No cached data exists
shouldUpdateSession = true;
}
// Only make API call when necessary
if (shouldUpdateSession) {
try {
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);
if (!response.ok) {
throw new Error(`API responded with status: ${response.status}`);
}
restaurantData = await response.json();
// Store data with timestamp
userSession.set("restaurant", JSON.stringify(restaurantData));
userSession.set("restaurant_timestamp", Date.now());
} catch (error) {
console.error('Restaurant API Error:', error);
// Fallback to cached data if available, or set error message
if (userSession.has("restaurant")) {
restaurantData = JSON.parse(userSession.get("restaurant") as string);
} else {
errorMsg = "Unable to load restaurant data. Please try again later.";
}
}
}
// Handle notifications and errors
if (userSession.has(“notification”)) {
notification = userSession.get(“notification”);
userSession.unset(“notification”); // Clear one-time notifications
shouldUpdateSession = true;
}
if (userSession.has(“errorMsg”)) {
errorMsg = userSession.get(“errorMsg”);
userSession.unset(“errorMsg”); // Clear one-time errors
shouldUpdateSession = true;
}
// Only update session when necessary
const headers: HeadersInit = {};
if (shouldUpdateSession) {
headers[‘Set-Cookie’] = await saveSession(userSession);
}
return json(
{ notification, errorMsg, restaurantData, consent },
{ headers }
);
}
Additional Production Optimizations
1. Disable Prefetching for Heavy Routes
Add this to your route component to prevent unnecessary prefetching:
javascript
export const handle = {
// Disable prefetching for this route
prefetch: “none”
};
2. Implement Server-Side Caching
For even better performance, consider using a server-side cache:
javascript
// Simple in-memory cache (use Redis in production)
const cache = new Map();
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour
function getCachedData(key: string) {
const cached = cache.get(key);
if (cached && (Date.now() - cached.timestamp) < CACHE_DURATION) {
return cached.data;
}
return null;
}
function setCachedData(key: string, data: any) {
cache.set(key, { data, timestamp: Date.now() });
}
3. Monitor Your API Usage
Add logging to track when API calls actually happen:
javascript
// Add this before your fetch call
console.log([${new Date().toISOString()}] Making API call for restaurant data);
Why This Solution Works
- Timestamp-Based Validation: Instead of relying solely on cookie expiration, we explicitly track when data was fetched
- Conditional Session Updates: We only update the session when actual changes occur
- Error Handling: Graceful fallback to cached data when API calls fail
- One-Time Message Clearing: Notifications and errors are properly cleared after being read
For the most comprehensive guide on Remix performance optimization and caching strategies, you absolutely must check out the official Remix documentation on Route Module API! It includes detailed explanations of loader behavior, prefetching controls, and advanced caching patterns that will save you hours of debugging time.
This approach should dramatically reduce your API calls while maintaining a smooth user experience. Your production API usage should now align much more closely with your actual visitor analytics!