{
  "attachments": [
    "./4x05_Remember_Me_title_card.webp",
    "./Dalen-Quaice-Never-Forget.jpg",
    "./remember5.jpg",
    "./remember-me-hd-323.jpg",
    "./Crusher_implicated_into_vortex.webp"
  ],
  "title": "Remember Me: The Case of the Vanishing Feeds",
  "tags": [
    "rss",
    "feedspool",
    "linkding",
    "golang",
    "homelab",
    "selfhosting",
    "indieweb"
  ],
  "type": "entry",
  "thumbnail": "/2026/08/22/vanishing-feeds/Crusher_implicated_into_vortex.webp",
  "year": "2026",
  "month": "08",
  "day": "22",
  "isDir": true,
  "slug": "vanishing-feeds",
  "date": "2026-08-22T19:00:00.000Z",
  "postName": "2026-08-22-vanishing-feeds",
  "html": "<p><strong>TL;DR</strong>: My self-hosted feed reading stack quietly shrank from hundreds of feeds down to 33 over the course of a year, and I didn't notice until my reading list got eerily quiet. The culprit: an unpaginated API call that turned \"the 1000 most recent bookmarks\" into a silent ceiling on my subscription list. I had been unsubscribing myself, one bookmark at a time.</p>\n<!--more-->\n\n\n\n<figure class=\"fullwidth\">\n<img src=\"./4x05_Remember_Me_title_card.webp\" alt=\"Title card for Star Trek: The Next Generation season 4 episode 5, &quot;Remember Me&quot; - the Enterprise-D against a starfield\" width=\"\" height=\"\">\n<figcaption>\n  Star Trek: The Next Generation, <a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">S04E05, \"Remember Me\"</a>\n</figcaption>\n</figure>\n\n<nav role=\"navigation\" class=\"table-of-contents\"></nav>\n\n<h2 id=\"rube-goldberg-feed-reader\">Rube Goldberg feed reader</h2>\n<p>So, I have what I think is a fun little self-hosted Rube Goldberg stack powering my feed reading these days. (And you know <a href=\"https://blog.lmorchard.com/2025/06/02/obsidian-rube-goldberg/\">I love</a> Rube Goldberg machines.)</p>\n<p>First, I use <a href=\"https://linkding.link/\">Linkding</a> to collect bookmarks. My feed subscriptions live in there, tagged <a href=\"https://links.lmorchard.com/bookmarks?q=%23feeds%3Asubscriptions\">feeds:subscriptions</a>.</p>\n<p>Next, I use <a href=\"https://github.com/lmorchard/linkding-to-opml\">linkding-to-opml</a> to produce an OPML file from those bookmarks. This is <a href=\"/2025/10/25/miscellanea/\">a little Go CLI tool I built</a>. It iterates through the tagged Linkding bookmarks, fetches the links, and figures out whether those links are themselves feeds or whether it needs to autodetect feeds from HTML pages. Then it builds an OPML file as a subscription list. I run this on a crontab schedule every half hour, so my subscription list stays current with my bookmarks.</p>\n<p>Then, I use <a href=\"https://github.com/lmorchard/feedspool-go\">feedspool-go</a> - <a href=\"https://blog.lmorchard.com/2026/01/26/feedspool-go-v0-2-0/\">another Go CLI tool that I built</a> - to fetch feeds from that OPML subscription list and build a static website as my personal RSS newspaper. I run this on the same half-hour schedule - every 30 minutes, a fresh build.</p>\n<figure>\n<mermaid-diagram>\ngraph TD\n  A[Me, saving bookmarks] --&gt; B[(Linkding)]\n  B --&gt;|tagged feeds:subscriptions| C[linkding-to-opml]\n  C --&gt;|cron, every 30 min| D[subscriptions.opml]\n  D --&gt; E[feedspool-go]\n  E --&gt;|cron, every 30 min| G[Static HTML newspaper]\n  G --&gt;|I read something good| A\n</mermaid-diagram>\n<figcaption>\n  The stack, and the loop that turned out to matter\n</figcaption>\n</figure>\n\n<p>This stack has served me pretty well for the past year or so, with just occasional bug fixes and tweaks along the way. For the most part, the cronjobs percolated away on one of my basement homelab machines and I never bothered to peek at the logs or otherwise check up on them.</p>\n<h2 id=\"the-quiet\">The quiet</h2>\n<p>But lately I'd noticed that my reading list seemed to be getting quieter and quieter.</p>\n<p>It was gradual enough that it took me until just last night to realize I was missing quite a few feeds from the regular rotation. It was kind of like that episode of Star Trek: The Next Generation - <a href=\"https://en.wikipedia.org/wiki/Remember_Me_(Star_Trek:_The_Next_Generation)\">S04E05, \"Remember Me\"</a> - where Dr. Crusher steadily loses people off the Enterprise but takes a while to work out that the problem is with her universe rather than her memory. 😅</p>\n<figure>\n<img src=\"./Dalen-Quaice-Never-Forget.jpg\" alt=\"Demotivational-poster meme of Dalen Quaice from the episode, captioned NEVER FORGET\" width=\"\" height=\"\">\n<figcaption>\n  <a href=\"https://reactormag.com/star-trek-the-next-generation-rewatch-remember-me/\">Pour one out for Dalen Quaice</a>, and for whichever feed went first.\n</figcaption>\n</figure>\n\n<p>Finally I ssh'd into the VM running the stack and took a look: only 33 feeds were getting fetched and published. I have 792 bookmarks tagged <code>feeds:subscriptions</code>. Where did they all go?!</p>\n<p>Granted, a good chunk of those subscriptions are rather ancient. I've been accumulating and shedding RSS feeds for decades now, and more than a few of those sites have dropped off the web in that time. But a drop from hundreds to 33 seems a bit much.</p>\n<h2 id=\"the-bug\">The bug</h2>\n<p>After a few hours of frustrated code-staring, I found it. Here's what <code>FetchBookmarks</code> looked like in linkding-to-opml:</p>\n<pre><code class=\"language-go\">// Use linkding client to fetch bookmarks\n// For now, we'll get all bookmarks and filter client-side\n// The go-linkding library may support server-side filtering in the future\nbookmarkList, err := c.client.ListBookmarks(linkding.ListBookmarksParams{\n    Limit:  1000, // Get lots of bookmarks (adjust as needed)\n    Offset: 0,\n})\n</code></pre>\n<p>Past me left a comment describing the bug and then shipped it anyway. 🤦‍♂️</p>\n<figure>\n<img src=\"./remember5.jpg\" alt=\"An LCARS console display reading SPACECRAFT STRUCTURAL ANALYSIS, showing a wireframe diagram of the Enterprise\" width=\"\" height=\"\">\n<figcaption>\n  <a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">LCARS console.</a> Roughly what an hour of staring at <code>client.go</code> felt like.\n</figcaption>\n</figure>\n\n<p>Two things are wrong here, and they compound. The first is that this asks for a single page of 1000 bookmarks and then ignores the API's <code>next</code> link entirely. Linkding never returns the whole collection in one response - it pages everything - so any bookmark outside the 1000 most recently added was simply invisible to the exporter.</p>\n<p>The second is that tag filtering ran <em>client-side</em>, over that already-truncated page. I never wired the tag search into the API query at all. So the page size wasn't just a limit on how much I fetched, it was a hard ceiling on how many tagged results could possibly come out the other end.</p>\n<p>I have 34,568 bookmarks in Linkding. The exporter was looking at 1000 of them. Of those, 33 happened to carry the <code>feeds:subscriptions</code> tag.</p>\n<p>And nothing errored. Nothing warned. The log line even reported a cheerful <code>total_fetched</code>, which looks perfectly healthy right up until you notice it was counting the length of the final page rather than the number of records actually read.</p>\n<h2 id=\"unsubscribing-one-bookmark-at-a-time\">Unsubscribing one bookmark at a time</h2>\n<p>Here's the part I find genuinely funny: that 1000-bookmark window is sorted newest-first, so it <em>slides</em>. When I first set this stack up, all my feed subscriptions happened to fall inside the window and everything worked great. But every new bookmark I saved pushed the oldest one out the back.</p>\n<figure>\n<img src=\"./remember-me-hd-323.jpg\" alt=\"Close-up of Dr. Beverly Crusher looking alarmed as she realizes something is wrong\" width=\"\" height=\"\">\n<figcaption>\n  \"<a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">Computer, how many feeds are in my subscription list?</a>\"\n</figcaption>\n</figure>\n\n<p>So for the past year, every time I bookmarked an article, I had a chance of silently unsubscribing myself from a feed. I wasn't losing feeds because sites died. I was losing them because I kept adding new stuff.</p>\n<p>The fix is boring: page until the collection is exhausted, advance the offset by the number of records actually received rather than the number requested (so a server that caps <code>limit</code> still pages correctly), and stop on an empty page as well as on reaching the reported count, so a wrong count can't spin the loop forever.</p>\n<p>I also pushed the tag filter into the API query while I was in there. It turns out, despite my earlier comment, the golang library supported that just fine. 🤷‍♂️ Linkding treats a <code>#tag</code> token as an exact match, ANDs multiple tokens together, and ignores case - the same contract my client-side <code>matchesTags</code> was implementing. So the filter can run server-side and transfer 792 records instead of 34,568. I kept <code>matchesTags</code> running over every result anyway, so if Linkding's query semantics ever drift from my assumptions, the failure mode is slow instead of wrong.</p>\n<h2 id=\"the-bug-behind-the-bug\">The bug behind the bug</h2>\n<p>Then, with pagination fixed, a second bug immediately surfaced.</p>\n<p><code>GenerateOPML</code> emitted one outline per successful feed discovery. But multiple bookmarks on the same site normally resolve to that site's single feed - three articles I saved from one blog are three bookmarks pointing at one <code>feed.xml</code>. So the OPML had duplicates, and any reader importing it would see the same feed listed several times.</p>\n<p>This had been latent the whole time. With 33 results, a collision was unlikely enough that I never saw it. Across 792 bookmarks, 159 feed URLs repeat, which produced 628 outlines for 464 distinct feeds. Fixing the first bug is what made the second one big enough to trip over.</p>\n<p>That one's fixed too now - keep the first result for each feed URL, so the <code>htmlUrl</code> a reader displays stays stable between runs.</p>\n<h2 id=\"anyway\">Anyway</h2>\n<p>Nice to solve a mystery, even if it's one I set for myself months ago. 464 feeds, and my reading list is nice and noisy again.</p>\n<p>The thing I keep turning over is how <em>comfortable</em> the failure was. A crashed cronjob would have gotten my attention in a day. A cronjob that succeeds, exits zero, logs a cheerful <code>total_fetched</code>, and produces a slightly smaller OPML file every week is invisible for a year. The stack never stopped working. It just kept working a little bit less, and I adjusted my expectations to match right along with it.</p>\n<p>Which is, come to think of it, exactly Dr. Crusher's problem.</p>\n<figure class=\"fullwidth\">\n<img src=\"./Crusher_implicated_into_vortex.webp\" alt=\"Dr. Crusher pulled off her feet and sucked sideways into a vortex on the Enterprise bridge\" width=\"\" height=\"\">\n<figcaption>\n  <a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">The static warp bubble collapses.</a> Eventually.\n</figcaption>\n</figure>\n\n",
  "body": "**TL;DR**: My self-hosted feed reading stack quietly shrank from hundreds of feeds down to 33 over the course of a year, and I didn't notice until my reading list got eerily quiet. The culprit: an unpaginated API call that turned \"the 1000 most recent bookmarks\" into a silent ceiling on my subscription list. I had been unsubscribing myself, one bookmark at a time.\n\n<!--more-->\n\n<figure class=\"fullwidth\">\n<img src=\"./4x05_Remember_Me_title_card.webp\" alt=\"Title card for Star Trek: The Next Generation season 4 episode 5, &quot;Remember Me&quot; - the Enterprise-D against a starfield\">\n<figcaption>\n  Star Trek: The Next Generation, <a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">S04E05, \"Remember Me\"</a>\n</figcaption>\n</figure>\n\n<nav role=\"navigation\" class=\"table-of-contents\"></nav>\n\n## Rube Goldberg feed reader\n\nSo, I have what I think is a fun little self-hosted Rube Goldberg stack powering my feed reading these days. (And you know [I love](https://blog.lmorchard.com/2025/06/02/obsidian-rube-goldberg/) Rube Goldberg machines.)\n\nFirst, I use [Linkding](https://linkding.link/) to collect bookmarks. My feed subscriptions live in there, tagged [feeds:subscriptions](https://links.lmorchard.com/bookmarks?q=%23feeds%3Asubscriptions).\n\nNext, I use [linkding-to-opml](https://github.com/lmorchard/linkding-to-opml) to produce an OPML file from those bookmarks. This is [a little Go CLI tool I built](/2025/10/25/miscellanea/). It iterates through the tagged Linkding bookmarks, fetches the links, and figures out whether those links are themselves feeds or whether it needs to autodetect feeds from HTML pages. Then it builds an OPML file as a subscription list. I run this on a crontab schedule every half hour, so my subscription list stays current with my bookmarks.\n\nThen, I use [feedspool-go](https://github.com/lmorchard/feedspool-go) - [another Go CLI tool that I built](https://blog.lmorchard.com/2026/01/26/feedspool-go-v0-2-0/) - to fetch feeds from that OPML subscription list and build a static website as my personal RSS newspaper. I run this on the same half-hour schedule - every 30 minutes, a fresh build.\n\n<figure>\n<mermaid-diagram>\ngraph TD\n  A[Me, saving bookmarks] --> B[(Linkding)]\n  B -->|tagged feeds:subscriptions| C[linkding-to-opml]\n  C -->|cron, every 30 min| D[subscriptions.opml]\n  D --> E[feedspool-go]\n  E -->|cron, every 30 min| G[Static HTML newspaper]\n  G -->|I read something good| A\n</mermaid-diagram>\n<figcaption>\n  The stack, and the loop that turned out to matter\n</figcaption>\n</figure>\n\nThis stack has served me pretty well for the past year or so, with just occasional bug fixes and tweaks along the way. For the most part, the cronjobs percolated away on one of my basement homelab machines and I never bothered to peek at the logs or otherwise check up on them.\n\n## The quiet\n\nBut lately I'd noticed that my reading list seemed to be getting quieter and quieter.\n\nIt was gradual enough that it took me until just last night to realize I was missing quite a few feeds from the regular rotation. It was kind of like that episode of Star Trek: The Next Generation - [S04E05, \"Remember Me\"](https://en.wikipedia.org/wiki/Remember_Me_(Star_Trek:_The_Next_Generation)) - where Dr. Crusher steadily loses people off the Enterprise but takes a while to work out that the problem is with her universe rather than her memory. 😅\n\n<figure>\n<img src=\"./Dalen-Quaice-Never-Forget.jpg\" alt=\"Demotivational-poster meme of Dalen Quaice from the episode, captioned NEVER FORGET\">\n<figcaption>\n  <a href=\"https://reactormag.com/star-trek-the-next-generation-rewatch-remember-me/\">Pour one out for Dalen Quaice</a>, and for whichever feed went first.\n</figcaption>\n</figure>\n\nFinally I ssh'd into the VM running the stack and took a look: only 33 feeds were getting fetched and published. I have 792 bookmarks tagged `feeds:subscriptions`. Where did they all go?!\n\nGranted, a good chunk of those subscriptions are rather ancient. I've been accumulating and shedding RSS feeds for decades now, and more than a few of those sites have dropped off the web in that time. But a drop from hundreds to 33 seems a bit much.\n\n## The bug\n\nAfter a few hours of frustrated code-staring, I found it. Here's what `FetchBookmarks` looked like in linkding-to-opml:\n\n```go\n// Use linkding client to fetch bookmarks\n// For now, we'll get all bookmarks and filter client-side\n// The go-linkding library may support server-side filtering in the future\nbookmarkList, err := c.client.ListBookmarks(linkding.ListBookmarksParams{\n\tLimit:  1000, // Get lots of bookmarks (adjust as needed)\n\tOffset: 0,\n})\n```\n\nPast me left a comment describing the bug and then shipped it anyway. 🤦‍♂️\n\n<figure>\n<img src=\"./remember5.jpg\" alt=\"An LCARS console display reading SPACECRAFT STRUCTURAL ANALYSIS, showing a wireframe diagram of the Enterprise\">\n<figcaption>\n  <a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">LCARS console.</a> Roughly what an hour of staring at <code>client.go</code> felt like.\n</figcaption>\n</figure>\n\nTwo things are wrong here, and they compound. The first is that this asks for a single page of 1000 bookmarks and then ignores the API's `next` link entirely. Linkding never returns the whole collection in one response - it pages everything - so any bookmark outside the 1000 most recently added was simply invisible to the exporter.\n\nThe second is that tag filtering ran *client-side*, over that already-truncated page. I never wired the tag search into the API query at all. So the page size wasn't just a limit on how much I fetched, it was a hard ceiling on how many tagged results could possibly come out the other end.\n\nI have 34,568 bookmarks in Linkding. The exporter was looking at 1000 of them. Of those, 33 happened to carry the `feeds:subscriptions` tag.\n\nAnd nothing errored. Nothing warned. The log line even reported a cheerful `total_fetched`, which looks perfectly healthy right up until you notice it was counting the length of the final page rather than the number of records actually read.\n\n## Unsubscribing one bookmark at a time\n\nHere's the part I find genuinely funny: that 1000-bookmark window is sorted newest-first, so it *slides*. When I first set this stack up, all my feed subscriptions happened to fall inside the window and everything worked great. But every new bookmark I saved pushed the oldest one out the back.\n\n<figure>\n<img src=\"./remember-me-hd-323.jpg\" alt=\"Close-up of Dr. Beverly Crusher looking alarmed as she realizes something is wrong\">\n<figcaption>\n  \"<a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">Computer, how many feeds are in my subscription list?</a>\"\n</figcaption>\n</figure>\n\nSo for the past year, every time I bookmarked an article, I had a chance of silently unsubscribing myself from a feed. I wasn't losing feeds because sites died. I was losing them because I kept adding new stuff.\n\nThe fix is boring: page until the collection is exhausted, advance the offset by the number of records actually received rather than the number requested (so a server that caps `limit` still pages correctly), and stop on an empty page as well as on reaching the reported count, so a wrong count can't spin the loop forever.\n\nI also pushed the tag filter into the API query while I was in there. It turns out, despite my earlier comment, the golang library supported that just fine. 🤷‍♂️ Linkding treats a `#tag` token as an exact match, ANDs multiple tokens together, and ignores case - the same contract my client-side `matchesTags` was implementing. So the filter can run server-side and transfer 792 records instead of 34,568. I kept `matchesTags` running over every result anyway, so if Linkding's query semantics ever drift from my assumptions, the failure mode is slow instead of wrong.\n\n## The bug behind the bug\n\nThen, with pagination fixed, a second bug immediately surfaced.\n\n`GenerateOPML` emitted one outline per successful feed discovery. But multiple bookmarks on the same site normally resolve to that site's single feed - three articles I saved from one blog are three bookmarks pointing at one `feed.xml`. So the OPML had duplicates, and any reader importing it would see the same feed listed several times.\n\nThis had been latent the whole time. With 33 results, a collision was unlikely enough that I never saw it. Across 792 bookmarks, 159 feed URLs repeat, which produced 628 outlines for 464 distinct feeds. Fixing the first bug is what made the second one big enough to trip over.\n\nThat one's fixed too now - keep the first result for each feed URL, so the `htmlUrl` a reader displays stays stable between runs.\n\n## Anyway\n\nNice to solve a mystery, even if it's one I set for myself months ago. 464 feeds, and my reading list is nice and noisy again.\n\nThe thing I keep turning over is how *comfortable* the failure was. A crashed cronjob would have gotten my attention in a day. A cronjob that succeeds, exits zero, logs a cheerful `total_fetched`, and produces a slightly smaller OPML file every week is invisible for a year. The stack never stopped working. It just kept working a little bit less, and I adjusted my expectations to match right along with it.\n\nWhich is, come to think of it, exactly Dr. Crusher's problem.\n\n<figure class=\"fullwidth\">\n<img src=\"./Crusher_implicated_into_vortex.webp\" alt=\"Dr. Crusher pulled off her feet and sucked sideways into a vortex on the Enterprise bridge\">\n<figcaption>\n  <a href=\"https://memory-alpha.fandom.com/wiki/Remember_Me_(episode)\">The static warp bubble collapses.</a> Eventually.\n</figcaption>\n</figure>\n\n",
  "parentPath": "./content/posts/2026/2026-08-22-vanishing-feeds",
  "path": "2026/08/22/vanishing-feeds",
  "summary": "TL;DR: My self-hosted feed reading stack quietly shrank from hundreds of feeds down to 33 over the course of a year, and I didn't notice until my reading list got eerily quiet. The culprit: an unpaginated API call that turned \"the 1000 most recent bookmarks\" into a silent ceiling on my subscription list. I had been unsubscribing myself, one bookmark at a time.",
  "needsBuild": true,
  "prevPostPath": "2026/08/07/w32",
  "prevPostTitle": "Smoky Skies, Raccoon Pool Parties, and Domain Negotiations (Week 32)"
}