diff --git a/package-lock.json b/package-lock.json
index e2f6fbd5e..c5b2f54c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
                 "express": "~4.19.2",
                 "express-basic-auth": "~1.2.1",
                 "express-static-gzip": "~2.1.7",
+                "feed": "^4.2.2",
                 "form-data": "~4.0.0",
                 "gamedig": "^4.2.0",
                 "html-escaper": "^3.0.3",
@@ -8315,6 +8316,18 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/feed": {
+            "version": "4.2.2",
+            "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz",
+            "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==",
+            "license": "MIT",
+            "dependencies": {
+                "xml-js": "^1.6.11"
+            },
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
         "node_modules/file-entry-cache": {
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -15925,6 +15938,18 @@
                 }
             }
         },
+        "node_modules/xml-js": {
+            "version": "1.6.11",
+            "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
+            "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
+            "license": "MIT",
+            "dependencies": {
+                "sax": "^1.2.4"
+            },
+            "bin": {
+                "xml-js": "bin/cli.js"
+            }
+        },
         "node_modules/xmlbuilder": {
             "version": "8.2.2",
             "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
diff --git a/package.json b/package.json
index 6476b7410..52ce4cac1 100644
--- a/package.json
+++ b/package.json
@@ -94,6 +94,7 @@
         "express": "~4.19.2",
         "express-basic-auth": "~1.2.1",
         "express-static-gzip": "~2.1.7",
+        "feed": "^4.2.2",
         "form-data": "~4.0.0",
         "gamedig": "^4.2.0",
         "html-escaper": "^3.0.3",
diff --git a/server/model/status_page.js b/server/model/status_page.js
index e40b28f6f..38f548ebb 100644
--- a/server/model/status_page.js
+++ b/server/model/status_page.js
@@ -5,6 +5,10 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
 const jsesc = require("jsesc");
 const googleAnalytics = require("../google-analytics");
 const { marked } = require("marked");
+const { Feed } = require("feed");
+const config = require("../config");
+
+const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util");
 
 class StatusPage extends BeanModel {
 
@@ -14,6 +18,24 @@ class StatusPage extends BeanModel {
      */
     static domainMappingList = { };
 
+    /**
+     * Handle responses to RSS pages
+     * @param {Response} response Response object
+     * @param {string} slug Status page slug
+     * @returns {Promise<void>}
+     */
+    static async handleStatusPageRSSResponse(response, slug) {
+        let statusPage = await R.findOne("status_page", " slug = ? ", [
+            slug
+        ]);
+
+        if (statusPage) {
+            response.send(await StatusPage.renderRSS(statusPage, slug));
+        } else {
+            response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
+        }
+    }
+
     /**
      * Handle responses to status page
      * @param {Response} response Response object
@@ -39,6 +61,38 @@ class StatusPage extends BeanModel {
         }
     }
 
+    /**
+     * SSR for RSS feed
+     * @param {statusPage} statusPage object
+     * @param {slug} slug from router
+     * @returns {Promise<string>} the rendered html
+     */
+    static async renderRSS(statusPage, slug) {
+        const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage);
+
+        let proto = config.isSSL ? "https" : "http";
+        let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`;
+
+        const feed = new Feed({
+            title: "uptime kuma rss feed",
+            description: `current status: ${statusDescription}`,
+            link: host,
+            language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+            updated: new Date(), // optional, default = today
+        });
+
+        heartbeats.forEach(heartbeat => {
+            feed.addItem({
+                title: `${heartbeat.name} is down`,
+                description: `${heartbeat.name} has been down since ${heartbeat.time}`,
+                id: heartbeat.monitorID,
+                date: new Date(heartbeat.time),
+            });
+        });
+
+        return feed.rss2();
+    }
+
     /**
      * SSR for status pages
      * @param {string} indexHTML HTML page to render
@@ -98,6 +152,109 @@ class StatusPage extends BeanModel {
         return $.root().html();
     }
 
+    /**
+     * @param {heartbeats} heartbeats from getRSSPageData
+     * @returns {number} status_page constant from util.ts
+     */
+    static overallStatus(heartbeats) {
+        if (heartbeats.length === 0) {
+            return -1;
+        }
+
+        let status = STATUS_PAGE_ALL_UP;
+        let hasUp = false;
+
+        for (let beat of heartbeats) {
+            if (beat.status === MAINTENANCE) {
+                return STATUS_PAGE_MAINTENANCE;
+            } else if (beat.status === UP) {
+                hasUp = true;
+            } else {
+                status = STATUS_PAGE_PARTIAL_DOWN;
+            }
+        }
+
+        if (! hasUp) {
+            status = STATUS_PAGE_ALL_DOWN;
+        }
+
+        return status;
+    }
+
+    /**
+     * @param {number} status from overallStatus
+     * @returns {string} description
+     */
+    static getStatusDescription(status) {
+        if (status === -1) {
+            return "No Services";
+        }
+
+        if (status === STATUS_PAGE_ALL_UP) {
+            return "All Systems Operational";
+        }
+
+        if (status === STATUS_PAGE_PARTIAL_DOWN) {
+            return "Partially Degraded Service";
+        }
+
+        if (status === STATUS_PAGE_ALL_DOWN) {
+            return "Degraded Service";
+        }
+
+        // TODO: show the real maintenance information: title, description, time
+        if (status === MAINTENANCE) {
+            return "Under maintenance";
+        }
+
+        return "?";
+    }
+
+    /**
+     * Get all data required for RSS
+     * @param {StatusPage} statusPage Status page to get data for
+     * @returns {object} Status page data
+     */
+    static async getRSSPageData(statusPage) {
+        // get all heartbeats that correspond to this statusPage
+        const config = await statusPage.toPublicJSON();
+
+        // Public Group List
+        const showTags = !!statusPage.show_tags;
+
+        const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
+            statusPage.id
+        ]);
+
+        let heartbeats = [];
+
+        for (let groupBean of list) {
+            let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
+            for (const monitor of monitorGroup.monitorList) {
+                const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
+                if (heartbeat) {
+                    heartbeats.push({
+                        ...monitor,
+                        status: heartbeat.status,
+                        time: heartbeat.time
+                    });
+                }
+            }
+        }
+
+        // calculate RSS feed description
+        let status = StatusPage.overallStatus(heartbeats);
+        let statusDescription = StatusPage.getStatusDescription(status);
+
+        // keep only DOWN heartbeats in the RSS feed
+        heartbeats = heartbeats.filter(heartbeat => heartbeat.status === DOWN);
+
+        return {
+            heartbeats,
+            statusDescription
+        };
+    }
+
     /**
      * Get all status page data in one call
      * @param {StatusPage} statusPage Status page to get data for
diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js
index 42cccc942..b209d33d1 100644
--- a/server/routers/status-page-router.js
+++ b/server/routers/status-page-router.js
@@ -18,6 +18,11 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
     await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
 });
 
+router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
+    let slug = request.params.slug;
+    await StatusPage.handleStatusPageRSSResponse(response, slug);
+});
+
 router.get("/status", cache("5 minutes"), async (request, response) => {
     let slug = "default";
     await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);