Caching & Performance

Feedframer caches API responses at the edge using Cloudflare. This page explains what is cached, for how long, when the cache is cleared, and how to tell how fresh your data is.

There are no rate limits on the API.

Two kinds of freshness

Two separate things decide how up to date a response is:

  1. How often Feedframer checks Instagram. Free accounts are checked daily and premium accounts hourly. The Refresh Posts button on the Dashboard triggers an extra check (once per 24 hours on Free, once per hour on Premium).
  2. The edge cache. A response can be served from Cloudflare's cache for up to an hour after it was generated. The cache is cleared automatically when a check finds new or changed posts (see below).

What is cached

Cached?
Successful GET responses from /api/v1/me, /api/v1/accounts and /api/v1/rss Yes, for up to 1 hour
Error responses (401, 404, 405, 422, 500) No
GraphQL (/graphql) No

Cached responses carry this header:

Cache-Control: max-age=3600, public

The cache is keyed on the full URL, including api_key and every other query parameter. Requests with different parameters are cached separately, so /api/v1/me?api_key=YOUR_API_KEY and /api/v1/me?api_key=YOUR_API_KEY&filter[type]=VIDEO each have their own cache entry. If you build URLs in code, build them the same way every time so repeat requests hit the same entry.

Each cached response is tagged with the account's username. That's how Feedframer clears all of an account's cached responses at once.

When the cache is cleared

An account's cached responses are cleared automatically when:

  1. A check finds new or changed posts. The next request gets the new data. A check that finds nothing new leaves the cache alone.
  2. The API key is regenerated. The old key stops working immediately, including for responses that were cached.
  3. The account is disconnected. Its key returns 401 INVALID_API_KEY immediately.

You can also clear it yourself: on the Dashboard, click Purge Cache on the account's card. The next request for each URL is generated fresh.

Knowing when a feed has stopped updating

A Feedframer account holds an Instagram access token. Instagram can withdraw that token at any time — most often when the account owner changes their Instagram password, or removes Feedframer under Settings → Apps and websites — and when it does, Feedframer can no longer fetch anything new.

This does not change the status code. You still get a 200 with the posts Feedframer last stored, so an embedded feed keeps rendering rather than going blank. Nothing new will ever arrive, though, and the feed can sit there serving months-old posts while looking perfectly healthy. In October 2026 that was true of roughly one connected account in five.

Every response from /api/v1/me, /api/v1/accounts, /api/v1/rss and /graphql carries the connection's state:

X-Feedframer-Account-Status: invalid_token
X-Feedframer-Feed-Stale: Instagram rejected our access token for @example on
  2026-09-14T08:11:00+00:00. These posts are frozen as of 2026-09-14T08:11:00+00:00
  and will not update until the account owner reconnects at
  https://feedframer.com/dashboard
X-Feedframer-Account-Status Meaning What to do
ready Healthy. Posts refresh on the account's plan interval. Nothing.
importing First import is still running. The feed may be empty or partial. Retry shortly.
refreshing_token A routine token refresh is in flight. Posts are current. Nothing.
invalid_token Instagram is refusing the token. The feed has stopped updating and these posts may be months old. The account owner reconnects at feedframer.com/dashboard. Reconnecting does not change the API key.

X-Feedframer-Feed-Stale is only sent when the connection is broken, so its mere presence is the signal — and because it is plain English, it is readable in curl -I or a browser's network tab without knowing anything about the field.

Both headers are CORS-exposed, so you can read them from browser JavaScript on another site, not only from server-side code:

const response = await fetch('https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY');

if (response.headers.get('X-Feedframer-Account-Status') === 'invalid_token') {
  // Still render the posts — they are real, just frozen.
  console.warn(response.headers.get('X-Feedframer-Feed-Stale'));
}

This matters most if you installed the feed on a client's site. The person who logs in to Feedframer sees a warning on their dashboard; you, holding only the API key, would otherwise have no way to know the feed had stopped. If you build client sites on Feedframer, log X-Feedframer-Feed-Stale somewhere you will actually see it.

Responses are cached at the edge for an hour, so a status change can take up to an hour to appear. The same status is also in the body of /api/v1/accounts.

Checking freshness with X-Last-Fetched-At

Every successful response from /api/v1/me, and from /api/v1/rss, includes an X-Last-Fetched-At header. It tells you when Feedframer last checked Instagram for this account, as an ISO 8601 timestamp, or Never if it hasn't checked yet:

X-Last-Fetched-At: 2026-09-30T19:50:34+00:00

Two things to know:

  • It's the time of the last check, not the time of the last new post. A recent value with no new posts means Instagram had nothing new.
  • On a cached response, it's the value from when the response was cached. Because a check that finds nothing new doesn't clear the cache, the real last check can be up to an hour later than the header says.

This example reads the header and shows how long ago the account was checked:

const url = 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY';

const response = await fetch(url);
const body = await response.json();

if (!response.ok) {
  // Every error body is JSON:API: { errors: [{ status, code, title, detail }] }
  const error = body.errors[0];
  throw new Error(`${error.code}: ${error.detail}`);
}

const lastFetched = response.headers.get('X-Last-Fetched-At');

if (lastFetched === 'Never') {
  console.log('Feedframer has not fetched this account yet.');
} else {
  const minutesAgo = Math.round((Date.now() - new Date(lastFetched).getTime()) / 60000);
  console.log(`@${body.username}: last checked ${minutesAgo} minutes ago, ${body.posts.length} posts`);
}

This works in the browser as well as in server-side code. X-Last-Fetched-At is listed in Access-Control-Expose-Headers, so a page on another site can read it. The same timestamp is also in the response body of /api/v1/accounts as data[0].attributes.lastFetchAt (null if the account hasn't been fetched yet), if you would rather not depend on a header.

Seeing the cache status

Responses from https://feedframer.com pass through Cloudflare, which adds its own headers:

  • CF-Cache-Status: HIT when the response came from the cache, MISS when it didn't but has now been cached.
  • Age: on cached responses, how many seconds ago the response was cached.

Check these from a terminal or server code. Unlike Feedframer's own headers they are not CORS-exposed, so they aren't readable from browser JavaScript on another site. See Cloudflare's cache responses reference for the full list of statuses.

curl -s -o /dev/null -D - "https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY" | grep -iE "cf-cache-status|^age|cache-control|x-last-fetched-at"

Tips

  • Don't add your own cache-busting parameters. A random query parameter creates a new cache entry on every request, so you never get a cached response. If you need fresh data now, click Purge Cache instead.
  • Cache on your side if you want. Feedframer checks Instagram hourly (Premium) or daily (Free), plus any manual refreshes, so caching responses in your app for a few minutes, or up to an hour, costs you little freshness.
  • Use GraphQL when you need uncached responses. /graphql isn't cached at the edge, but it still reflects Feedframer's last check of Instagram, not Instagram in real time.