RSS a 66 redes sociales: Facebook, Instagram, X, LinkedIn, Telegram y más Blog Afiliados Contactos
Iniciar sesión Empezar gratis
Updated: 2026-10-25
Quart RSS Feed in Python: Async Route and Valid XML

Short answer: To serve an RSS feed from a Quart application, add an async GET route for /feed.xml, load the newest published rows with an asynchronous database driver, build the document with the feedgen library so every value is escaped, use timezone-aware UTC dates and return the bytes with the application/rss+xml content type. Hash the finished body into an ETag, answer a matching If-None-Match header with a 304, take the last build date from the newest item and not from the clock, test the route with the built-in test client, curl and a validator, then give the public address to an auto-poster.

Quart is the asynchronous sibling of Flask. It keeps the familiar decorators, blueprints, templates and request objects, and adds native async and await, so a single worker can wait on many database calls and HTTP requests at once. Teams choose it when they want to move a Flask-style service to asyncio without relearning the framework, or when they build new services that call several APIs per request. Sooner or later such a service publishes something that people want to follow: articles, release notes, listings or offers. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. Quart has no feed support of its own and needs none. This guide builds a complete Quart RSS feed with an asynchronous query, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.

When a Quart service needs a feed

A feed is worth adding whenever new items appear on a schedule you do not control: a changelog, a directory of listings, a news section or a library of guides. Readers who follow it in a feed reader get every item in order. Search engines and aggregators discover new pages faster. And an auto-poster such as PostRSS can share each new item on social networks the moment it is published, which turns a quiet publishing routine into a steady stream of visitors.

If your content already lives in a blog or a CMS, use the feed that system provides instead; see how to find your WordPress feed link. If you use the synchronous original, the same ideas are covered in the Flask RSS feed guide, and for other asynchronous Python frameworks see the Sanic guide y the FastAPI guide. Build the route yourself when the content is in your own database and no ready-made feed exists.

Set up the project

Create a virtual environment and install Quart, the feedgen library that writes valid RSS and Atom documents, and aiosqlite for the example database. Any asynchronous driver works the same way.

python -m venv .venv
source .venv/bin/activate
pip install quart feedgen aiosqlite pytest pytest-asyncio

Assume a table named articles with the columns slug, title, summary, image_url, image_bytes, image_type and published_at. The last column holds a UTC unix timestamp, which keeps comparisons simple and avoids time zone surprises. Items scheduled for the future simply have a timestamp that has not arrived yet.

The complete application

The program below defines the query, builds the document once per request and sends it with a strong validator. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website.

import hashlib
import os
from datetime import datetime, timezone

import aiosqlite
from feedgen.feed import FeedGenerator
from quart import Quart, Response, request

SITE = os.environ.get("SITE_URL", "https://example.com")
DB_PATH = os.environ.get("DB_PATH", "site.db")

app = Quart(__name__)


async def load_items(limit: int = 20):
    now = int(datetime.now(timezone.utc).timestamp())
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        cursor = await db.execute(
            "SELECT slug, title, summary, image_url, image_bytes, image_type, published_at "
            "FROM articles WHERE published_at <= ? ORDER BY published_at DESC LIMIT ?",
            (now, limit),
        )
        return await cursor.fetchall()


def build_feed(rows) -> bytes:
    fg = FeedGenerator()
    fg.title("Example Journal")
    fg.link(href=SITE + "/", rel="alternate")
    fg.link(href=SITE + "/feed.xml", rel="self", type="application/rss+xml")
    fg.description("News and guides from Example Journal.")
    fg.language("en")
    newest = rows[0]["published_at"] if rows else 0
    fg.lastBuildDate(datetime.fromtimestamp(newest, tz=timezone.utc))

    for row in rows:
        url = f"{SITE}/articles/{row['slug']}/"
        entry = fg.add_entry(order="append")
        entry.id(url)
        entry.guid(url, permalink=True)
        entry.title(row["title"])
        entry.link(href=url)
        entry.description(row["summary"])
        entry.pubDate(datetime.fromtimestamp(row["published_at"], tz=timezone.utc))
        if row["image_url"]:
            entry.enclosure(
                row["image_url"],
                str(row["image_bytes"] or 0),
                row["image_type"] or "image/jpeg",
            )
    return fg.rss_str(pretty=False)


def etag_matches(header: str | None, etag: str) -> bool:
    if not header:
        return False
    for token in header.split(","):
        token = token.strip().removeprefix("W/")
        if token == "*" or token == etag:
            return True
    return False


@app.get("/feed.xml")
async def feed():
    rows = await load_items()
    body = build_feed(rows)
    etag = '"' + hashlib.sha256(body).hexdigest()[:32] + '"'
    headers = {"ETag": etag, "Cache-Control": "public, max-age=300"}
    if etag_matches(request.headers.get("If-None-Match"), etag):
        return Response(status=304, headers=headers)
    return Response(body, mimetype="application/rss+xml", headers=headers)


if __name__ == "__main__":
    app.run(port=8000)

Save it as app.py and start it with python app.py for development, or with an ASGI server such as Hypercorn in production. The route accepts GET, and Quart adds HEAD automatically for routes that allow GET, which is what some validators and monitoring tools use first.

Why the query is asynchronous

Quart handlers are coroutines that share one event loop, just like in other asyncio frameworks. A synchronous database call inside a handler would block the loop for as long as the query takes, and every other request would wait behind it. The aiosqlite driver runs the work in a thread and exposes it with await, so the loop stays free. With PostgreSQL or MySQL, use an asynchronous driver in the same way. If you must call a synchronous library, wrap the call with asyncio.to_thread. Keep the query small: twenty rows, one table, an index on the publication column.

Why feedgen and not string building

The feedgen library knows the structure of RSS and Atom. It escapes titles and summaries, writes the namespaces, formats dates in the RFC 822 style from timezone-aware datetime objects and refuses naive datetimes, which prevents the most common date bug. It also lets you produce an Atom version of the same feed with fg.atom_str, if some of your readers prefer it. The library is synchronous, but building twenty items takes microseconds, so it is safe to call inside a handler.

How the ETag logic works

Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. A validator lets them ask a cheap question: has the document changed since the version I hold? The handler hashes the finished bytes, quotes the hash as an entity tag and compares it with the If-None-Match header. A match produces a 304 response with no body, which saves bandwidth for both sides and keeps your server calm.

The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the example takes the last build date from the newest item and not from the clock. If you wrote the current time into that element, the hash would change on every request, no client would ever receive a 304 and the validator would be useless. The same rule applies to any value generated at request time. A compressing proxy may turn a strong tag into a weak one, which is why the comparison above strips the W/ prefix.

Where each field comes from

Feed elementSourceRule
titleArticle titlePlain text, escaped by the library
linkPublic address of the pageAbsolute, with https, returning 200 directly
guidSame address, or a stored identifierNever changes after publication
pubDatePublication timestampAware UTC datetime, never the current time
descriptionShort summaryOne or two sentences of plain text
enclosureFeatured imageAddress, size in bytes and media type

The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. If you may rename slugs, store a separate permanent identifier in the database and pass it to the guid method with permalink set to false.

Caching without delaying new posts

The Cache-Control header in the example lets shared caches keep the response for five minutes, and the ETag lets everyone revalidate cheaply after that. If you place a content delivery network in front of the application, keep the lifetime short so a new item does not wait long for its turn. A feed that lags behind the website by an hour defeats the purpose of automation. For the background, read how auto-posting tools use ETags and conditional requests y what 301, 304, 404 and 500 mean for auto-posting.

Test the feed before you connect it

A few minutes of testing prevents days of confusion later. Start with an automated test that proves the conditional logic, using the test client that ships with Quart.

import pytest
from app import app


@pytest.mark.asyncio
async def test_feed_supports_conditional_requests():
    client = app.test_client()
    first = await client.get("/feed.xml")
    assert first.status_code == 200
    assert first.headers["Content-Type"].startswith("application/rss+xml")
    etag = first.headers["ETag"]
    second = await client.get("/feed.xml", headers={"If-None-Match": etag})
    assert second.status_code == 304

Then check the running service from the outside:

  • Run curl -i https://example.com/feed.xml and confirm a 200 status, the right content type and an ETag header.
  • Repeat the request with the tag you received in an If-None-Match header and confirm that the answer is 304.
  • Paste the public address into the W3C Feed Validation Service and fix every error.
  • Publish a test item, wait for the cache lifetime and confirm that it appears once, at the top.

There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.

Running Quart in production

Run the application with an ASGI server such as Hypercorn behind a reverse proxy that terminates HTTPS, and make sure the proxy passes the real host and scheme so that absolute links are correct. Keep the site address in configuration, as the example does with an environment variable, so a staging copy never publishes production links. Add a health route that does not touch the database and monitor the feed route separately, because a feed can fail while the home page still works. If the route sits behind a login or a firewall rule for unknown clients, an auto-poster will not reach it; the feed must be public.

Common mistakes to avoid

  • Blocking the event loop. A synchronous query in the handler slows every request; use an asynchronous driver or asyncio.to_thread.
  • Current time in the body. It changes the hash on every request and disables the 304 answer.
  • Unstable GUIDs. Changing them makes auto-posters share old stories again; see why a feed auto-posts the same article twice.
  • Future items in the feed. Always filter on the publication time.
  • Wrong content type. A feed sent as text or HTML confuses some readers; see the content type header guide.
  • Naive datetimes. Dates without a zone are rejected by feedgen; always attach UTC.

How PostRSS works with a Quart feed

PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed route, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.

Lectura relacionada

The bottom line

Quart needs only an async route, a query that does not block the loop and a feed library to serve a dependable RSS feed: select published items dated in the past, let the library escape every value, use a stable GUID, attach UTC to every date, derive the last build date from the newest item and send the result with the right content type. Add a hash-based ETag so repeat requests end in a 304. Test the route with the test client, curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.

FAQ

Does Quart have a built-in RSS generator?

No. Quart has no feed support, but a library such as feedgen produces valid RSS and Atom from a few method calls, and the route returns the bytes with the application/rss+xml content type.

Is Quart different from Flask for a feed?

The route looks almost the same. The difference is that handlers are coroutines, so database calls should be awaited with an asynchronous driver instead of blocking the event loop.

Why does my feed never return 304?

Most often the body changes between requests, for example because the last build date is the current time. Take that date from the newest item and the hash stays the same until the content changes.

How many items should the feed contain?

Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.

Can I use this feed with PostRSS?

Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.

New guides, once a month

What changed in the networks, what broke, and how to fix it before it costs you reach.

We send a confirmation e-mail first. Unsubscribe any time.

Más herramientas de nuestro equipo

Creadas por Internet Solutions, el equipo detrás de PostRSS. Cada una te ahorra tiempo a su manera.