
The first version usually works. Point a script at an RSS feed, split on <item>, grab the text between <title> tags, and it runs clean against the one blog you tested against. Then a second feed breaks it — an unescaped ampersand in a URL, an some CMS forgot to re-encode, an undeclared namespace prefix, a <description> that turns out to hold a full <div> of unescaped HTML. Parsing RSS feeds in code looks solved because the spec is short. It isn’t, because almost nobody who publishes one has read it, and the format lets them get away with that.
Why Feeds Resist Naive Parsing
A feed is XML, so the instinct is to reach for string splitting or a regular expression instead of a real parser, on the theory that a feed is simple enough not to need one. That instinct never survives real data. Regexes can’t reliably handle nested or malformed markup, and feeds in the wild routinely ship exactly the malformations that break them: unescaped ampersands in query-string URLs, undeclared HTML entities, mismatched tags, stray byte-order marks, a Content-Type header that contradicts the XML declaration inside the document itself. The W3C Feed Validator has a named error for the single commonest case, Undefined Named Entity: a feed using when only entities predefined in the XML spec (&, <, >, ', ") are legal there. is an HTML entity, not an XML one — any CMS piping rendered HTML into a feed without re-encoding it produces a document that renders fine in a browser and fails XML parsing outright.

Layered on top: namespace sprawl. A real feed usually isn’t bare RSS or bare Atom — it’s RSS plus Dublin Core (dc:creator), the iTunes podcast namespace, Media RSS, a custom content:encoded element, sometimes several vendor extensions at once. A parser that only knows the base spec silently drops the rest. Add CDATA sections smuggling raw HTML past escaping, encoding declarations that lie about the actual byte encoding, and dates in two mutually unintelligible grammars, and “just parse the XML” stops being adequate advice fast.
Three Formats, One Job
Any real consumer handles three related-but-different documents. RSS 2.0 is the common case: an <rss> root wrapping one <channel>, with exactly three required channel elements — title, link, description — and no required item elements. The spec’s own text: “all elements of an item are optional, however at least one of title or description must be present.” Everything a consumer actually relies on — link, pubDate, guid — is optional in the spec and merely conventional in practice, which is exactly why so many feeds omit one.

RSS 1.0 is the one people don’t expect. Despite the name, it predates RSS 2.0 by two years and isn’t the same lineage — Harvard’s own version history calls it “a brand-new format, not related to any previous format.” It’s RDF/XML: items are RDF resources identified by a rdf:about URL and referenced from an <rdf:Seq> list, not flat child elements. A parser written only against RSS 2.0’s shape won’t make sense of it.
Atom (RFC 4287, Dec 2005) is the newest and strictest, and it is not “RSS 3” — a claim that shows up even in respectable tooling but isn’t true; it’s an independent IETF standard with its own registered MIME type, application/atom+xml, versus RSS’s merely conventional application/rss+xml. Atom makes mandatory precisely what RSS leaves optional: exactly one atom:id per entry (permanent, distinct from any link), one atom:title, one atom:updated. RSS is easy to write and ambiguous to read; Atom is more verbose to write and unambiguous to consume — opposite bets about who absorbs the complexity.
The Fields That Don’t Line Up
Consuming “a feed” as one abstraction means reconciling vocabularies that don’t map one-to-one:
- Identity. RSS’s
<guid>is optional, and itsisPermaLinkattribute defaults to true — omitting it silently asserts the guid is a working URL. Atom’satom:idis mandatory and explicitly separate from any link. RSS 1.0 identifies an item by itsrdf:aboutURL. Key “have I seen this” on the wrong field, or trust a guid that turns out to be a session-tagged URL, and a site redesign shows readers every old post as new. - Timestamps. RSS’s
pubDateis optional; Atom’satom:updatedis mandatory, withatom:publishedoptional and extra. - Body. RSS’s
<description>might be a two-line summary or the whole post — the spec doesn’t say which. Full-text RSS more correctly usescontent:encoded, borrowed from the RSS 1.0 Content module. Atom sidesteps the guesswork with typed<content type="html">, alongside an optional<summary>.
A sane consumer picks one precedence order per concern, everywhere: identity from id → guid → link; content from content:encoded → atom:content → description → atom:summary; timestamp from published/pubDate → updated. That quietly-maintained table is most of what “supporting RSS and Atom” means in a real codebase.
Dates Are a Silent Failure Surface
This trap costs the most debugging time because it never throws. RSS 2.0 dates follow RFC 822: Sat, 16 Aug 2026 09:30:00 +0000. Atom and JSON Feed follow RFC 3339 — a profile of ISO 8601, not a synonym for it — written 2026-08-16T09:30:00Z. The two share no syntax. Feed either format to a parser expecting the other and the document is still well-formed XML; nothing errors. The date field just fails to parse, the consumer falls back to “now,” and an entire archive silently lands at the top of the timeline as if freshly published.
RFC 822 hides three more edges: two-digit years are legal but ambiguous (always emit four); single-letter military time zones are specified incorrectly and should never be used, except Z; day and month names are English abbreviations only, so a non-English locale can emit a date that quietly breaks every strict downstream parser. Apple’s podcast requirements specify “RFC 2822” formatting, the modernized successor to RFC 822 and a stricter subset of it — worth citing correctly rather than picking one RFC number and hoping.
Let a Library Take the Punishment
None of this is an argument for hand-rolling a parser — it’s the argument against it. A format anyone can emit will be emitted badly by a meaningful share of publishers, and at consumer scale that permissiveness becomes your problem. The right move is handing malformed input to a library that has already absorbed years of real-world garbage.

In Python that’s feedparser — current release 6.0.14 (30 July 2026, BSD-2-Clause, Python 3.10+). Its defining feature is the bozo bit: instead of raising on malformed XML, it sets d.bozo = 1, stashes the parse error in d.bozo_exception, and still returns everything it recovered — a better contract for input from thousands of producers you don’t control than “throw and stop”:
import feedparser d = feedparser.parse(feed_url, etag=saved_etag, modified=saved_modified)if d.bozo: log.warning("not well-formed: %s", d.bozo_exception) for entry in d.entries: uid = entry.get("id") or entry.get("guid") or entry.get("link") when = entry.get("published_parsed") or entry.get("updated_parsed") content = entry.get("content") body = content[0].value if content else entry.get("summary", "") saved_etag, saved_modified = d.get("etag"), d.get("modified")That one call already does most of what this article describes: detects the format, normalises two incompatible date grammars into one struct_time, and — by default — runs body content through an allow-list HTML sanitizer, because feed bodies are untrusted third-party HTML and handing raw XML straight to a WebView is a stored-XSS bug with a subscription mechanism attached.
The same shape exists outside Python. Go has gofeed (v1.4.2, 20 August 2026, MIT), normalising RSS, Atom and JSON Feed into one model and describing its own handling of broken input as “best-effort.” Java has ROME; Node has rss-parser — both real and widely used, though their exact current versions weren’t independently verifiable at time of writing. The specific library matters less than the shared property: someone else already fought the encoding ladder and the two date grammars, and shipped the fight as a dependency.
Poll Politely — Conditional Requests, Not a Small DDoS
Consuming a feed is usually a recurring fetch, which turns parsing into an HTTP-manners problem. A poller that re-downloads the full document on every check adds up: one client pulling a 100 KB feed every sixty seconds, unconditionally, moves roughly 4 GB a month from a single origin for no reason — and a reader with ten thousand subscribers doing the same multiplies that against sites that never asked for it.
The fix is HTTP, not RSS: conditional requests, defined in RFC 9110. A server sending an ETag or Last-Modified header lets the next request echo it back as If-None-Match or If-Modified-Since. If nothing changed, the server replies 304 Not Modified — a response that, by spec, carries no body — instead of resending the whole feed. feedparser exposes this directly: pass the etag/modified values saved from the prior call, and a 304 shows up as d.status == 304 with no entries. RSS’s own throttling hints, <ttl> and <skipDays>, never got real adoption among aggregators; conditional HTTP requests, honoring 429 Too Many Requests with backoff, and identifying your poller with a real User-Agent are what a well-behaved consumer actually does.
WebSub: Skip Polling Entirely
Conditional GETs make polling cheap; they don’t make it instant, and any polling interval leaves a window where new content exists but hasn’t been noticed. WebSub — a W3C Recommendation first published 23 January 2018 and revised 2 June 2026, formerly PubSubHubbub — is the alternative: push, layered on top of ordinary feeds rather than replacing them.

Three roles: a publisher pings a hub on new content; a subscriber registers a callback URL with that hub in advance. Discovery happens via HTTP Link headers (rel="hub", rel="self") on the feed response, with <link> elements as a fallback. Subscribing is a short handshake: the subscriber POSTs hub.callback/hub.topic/hub.mode=subscribe; the hub replies 202 Accepted, then GETs the callback with a random challenge the subscriber must echo back to prove control of the endpoint. After that, the hub POSTs new content to the callback as it’s published — no polling loop required.
The honest cost: WebSub needs a publicly reachable HTTPS endpoint that can receive that verification GET and later POSTs, plus a hub willing to relay the feed — reintroducing exactly the intermediary a pull-based, no-account format was meant to avoid. Worth it for an aggregator watching thousands of feeds; usually not worth building for a weekend script checking three sources, where conditional polling on a sane interval is simpler and good enough.
Frequently Asked Questions
Do I need to support RSS 1.0 too? Rarely by choice, but a general-purpose parser handles it (and older RSS 0.9x) for free, so there’s little reason to special-case it out. Real-world feeds are overwhelmingly RSS 2.0 or Atom.
Is it safe to render <description> or <content> directly? No — feed content is third-party HTML, and rendering it unsanitized in a browser or WebView is a stored-XSS vector. feedparser sanitizes by default; a hand-rolled parse does not, and needs its own sanitization step first.
What about JSON Feed? A newer, smaller-adoption alternative (v1.0 in 2017, v1.1 in 2020) using plain JSON, RFC 3339 dates, and an id-stability rule as plain as Atom’s. Worth supporting if a source you follow emits it; not yet a reason to drop RSS or Atom.
How often should I poll? Whatever interval fits, as long as every request after the first is conditional — there’s no real bandwidth cost to polling every few minutes if 304s are doing their job. The argument is against polling unconditionally, not against polling often.
My parser throws on a feed that renders fine in a browser — why? Almost always an HTML entity like that isn’t legal XML, a Content-Type header disagreeing with the document’s own encoding, or an undeclared namespace prefix. Browsers forgive malformed markup far more than an XML parser is required to — that gap is exactly why feeds “work” for publishers and break for consumers.
The mechanics above — formats, field mismatches, date grammar, polling manners — are the developer-facing half of this subject. The broader story, of how RSS went from a consumer product to invisible infrastructure and why “RSS is dead” doesn’t hold up, is covered in this site’s companion piece on what RSS actually is. This one is the version for whoever has to make a feed work in code, not in a reader.
📚 Recommended Resources
Disclosure: some links below are Amazon affiliate links. If you buy through them, I earn a small commission — at no extra cost to you — which helps keep this site running.
The best references for the mechanics in this article are free: the RSS 2.0 Specification, the RSS Best Practices Profile, RFC 4287 (Atom), and the feedparser documentation are all short, readable, and cost nothing. The books below are worth owning as a desk reference once you’re building something bigger than a script — with one honest caveat: no post-2006 RSS-specific book exists, because the format stopped changing enough to justify one. That’s not a gap in this list; it’s a fact about the subject.
RSS and Atom in Action
Harold, Manning
The best of the format-mechanics books and the closest match to this article’s own territory — publishing and consuming both formats, written by someone who clearly hit the same <guid> and date-format traps covered above. Dated in its web-framework specifics, but the parsing chapters hold up because the underlying formats haven’t moved.
Developing Feeds with RSS and Atom
O’Reilly
A tighter, more spec-focused companion to the article above — useful specifically for the RSS-vs-Atom element-by-element differences this piece leans on, if you want them laid out in more depth than a blog post allows.
Hacking RSS and Atom
Wrox
The “build something with it” book rather than the “understand the spec” book — aimed at people writing consumers and aggregators rather than just publishing a feed. The natural next step after the code in this article.