getNodi

Templates / Feed refresh

Polling a feed on a sensible cycle

Polling is still how most of the web notifies you. Fifteen minutes is the usual compromise between freshness and being rude to somebody else’s server.

The recipe

Request
POST https://{{domain}}/api/feeds/refresh
Schedule
Every 15 minutes (*/15 * * * *)
Timeout
30s
Attempts
3
Counts as success
2xx

You supply

Your domain
example.com

Host only — no https:// and no trailing slash.

Why this job exists

Push is better than pull and is frequently unavailable. RSS and Atom have no push mechanism, most partner APIs offer no webhooks, and plenty of the ones that do will not send them to a small integrator. Polling is what remains.

The failure mode is doing it badly. Polling every minute against a feed that updates twice a day is thousands of pointless requests, and it is how integrations get rate-limited or blocked — an outcome that is tedious to reverse because it usually involves emailing somebody.

The other failure is polling without conditional requests. A feed sends its full body on every fetch unless you ask properly, so a naive poller downloads the same payload ninety-six times a day.

Setting it up

Store the ETag and Last-Modified from each response and send them back on the next request. A feed that has not changed then answers 304 with no body, which is faster for you and dramatically cheaper for the publisher:

Conditional polling

const cached = await state.get('feed:example');

const response = await fetch(FEED_URL, {
  headers: {
    ...(cached?.etag && { 'If-None-Match': cached.etag }),
    ...(cached?.lastModified && { 'If-Modified-Since': cached.lastModified }),
  },
});

if (response.status === 304) {
  return Response.json({ changed: false });
}

const items = parse(await response.text());
await ingest(items);

await state.set('feed:example', {
  etag: response.headers.get('etag'),
  lastModified: response.headers.get('last-modified'),
});

return Response.json({ changed: true, items: items.length });

Deduplicate on ingest by the feed item GUID rather than trusting the publication date. Plenty of feeds republish items with a new timestamp after an edit, and date-based deduplication turns one edit into a duplicate post.

Checking it actually works

  • Confirm you are receiving 304 responses between updates — if every fetch returns 200 with a body, the conditional headers are not being sent or not being honoured.
  • Check that a republished item updates rather than duplicating.
  • Read the publisher’s rate limits and any polling guidance in their robots or docs before shortening the cadence.

Where it goes wrong

Polling far faster than the feed changes

Match the cadence to the publication rhythm. A news wire justifies minutes; a company blog does not, and hourly is plenty. Fast polling on a slow feed is the behaviour that gets an integration blocked.

Reprocessing everything on every fetch

Without a stable identifier for what you already have, each poll re-ingests the whole window. Track the GUIDs and process only what is new.

Trusting the feed to be well-formed

Feeds break — unescaped entities, truncated XML, an HTML error page served with an XML content type. Parse defensively and fail the run rather than ingesting garbage.

Questions

How often is too often?

If the publisher documents a limit, follow it. Otherwise, roughly a quarter of the shortest interval between updates is a fair rule, and slower is rarely a problem.

What about WebSub or webhooks?

Use them where offered — they are strictly better. Keep a slow poll as a backstop, because push mechanisms drop messages and rarely tell you.

Can I poll several feeds in one job?

You can, but one job per feed gives you per-feed history and alerting, so a single broken publisher does not fail the whole run and hide the others.

Running it here

This is a preset in the product, not an illustration. Pick it in the dashboard, fill in the one value above, and the job is created, scheduled and enabled — with retries, a record of every run showing status, latency and response, and an alert the first time one fails. Secret values are encrypted at rest and never rendered back.

The free plan runs 3 jobs hourly and needs no card. Where a recipe wants a finer cadence than your plan allows, it is slowed to the fastest schedule you are permitted rather than rejected.