Epic Games does not publish a patch calendar for Fortnite. There is no “next update: October 15” page you can bookmark. Instead, the only reliable signal sits inside the Epic Games Public Status feed and a handful of third-party JSON endpoints that mirror it. On October 1, 2026, Epic rolled out update v42.30 with scheduled downtime at 4:00 a.m. ET (8:00 a.m. UTC), the same window it used three weeks earlier for v42.10 on September 3. Both times, matchmaking was disabled 30 minutes ahead of the maintenance window, and both times, players found out only because someone was watching the status page or a community tracker.

This tutorial builds that “someone” for you. Instead of refreshing a status page or trusting a leaker’s guess, you will write a small Python service that polls Epic’s own status API and Fortnite-API.com, detects the moment something changes, and pushes a notification to Discord before most players even see a loading screen. The project takes about 45 minutes to build and runs forever afterward on a $0 cron job or a free-tier server.

This is aimed at anyone who’s tired of guessing: competitive players who need to know the moment a patch window opens so they can finish a ranked session first, community managers running a Discord server who want one bot doing the announcing instead of five people screenshotting the same tweet, and developers who just want a small, concrete project for practicing API polling, state management, and webhook notifications. You don’t need prior Fortnite modding experience or any Epic developer account to follow along.

Why “when is the next Fortnite update” has no fixed answer

Searches for “when is the next Fortnite update” spike every few days because the honest answer keeps moving. Fortnite ships on three different cadences at once: scheduled season content drops, numbered patches like v42.30, and silent hotfixes that never get a version bump at all. Chapter 7, Season 4 (“Override”) began August 20, 2026, and as of October 2, Epic’s status dashboard showed no open incidents, with v42.30 marked complete after its October 1 rollout. Third-party schedule trackers list follow-up dates of October 15 and October 29, but those are projections, not confirmed Epic announcements, and treating them as fact is how half the “Fortnite down” panic posts get started.

That gap between rumor and confirmation is exactly what an automated watcher closes. Epic’s status page documentation describes an API that can “get a summary of the status page, including a status indicator, component statuses, unresolved incidents, and any upcoming or in-progress scheduled maintenances.” That single endpoint is the ground truth. Everything else, including this article’s code, is just a way of asking that endpoint a question every few minutes and caring when the answer changes.

Fortnite’s version numbering also matters for anyone automating this. In the September-October 2026 window, builds followed a major.minor pattern (v42.10, then v42.30), skipping v42.20 in public release notes, a reminder that you should never hard-code an assumption about sequential version numbers. Detect state changes, not number patterns.

Understanding Epic’s status indicator levels

Before you write a line of code, it helps to know what the status API is actually telling you, because not every value deserves a phone buzz at 2 a.m. The summary endpoint returns one of four indicator strings: none, minor, major, or critical. Each one maps to a different real-world impact, and building alert logic that treats all four the same way is how people end up muting their own monitoring bot within a week.

None and minor: usually safe to treat as routine

“None” is the default state and means every tracked component is healthy. “Minor” typically covers things like elevated error rates on a single service or a short, low-impact blip that resolves on its own within minutes. Scheduled maintenance windows, including the routine downtime Epic used for both v42.10 and v42.30, often register as “minor” rather than “major” because Epic plans for them in advance and expects a quick return to normal. Your watcher should still log these, since they’re exactly the signal you’re trying to catch, but you don’t need a siren-level alert for them.

Major and critical: the ones worth an immediate alert

“Major” and “critical” indicate broader outages, think login failures across the board or matchmaking down network-wide outside a planned window. These are the events that actually ruin someone’s gaming session, and they’re rarer than scheduled maintenance. If you want to get fancy later, you can have notify_discord() accept a severity flag and use Discord’s embed formatting to color-code messages red for major/critical and gray for minor/none, so the people reading your channel can triage at a glance instead of reading every line.

What you will build

By the end of this guide you will have a single Python script, fn_update_watch.py, that does five things on a loop:

  • Polls the Epic Games status API for Fortnite’s component status and any scheduled maintenance windows
  • Polls Fortnite-API.com’s news endpoint and hashes the response to catch content changes that don’t show up as a formal “incident”
  • Compares each poll against the last known state stored in a local JSON file
  • Posts a formatted alert to a Discord channel the moment a real change is detected
  • Logs every poll, retry, and alert to a rotating log file so you can audit what happened overnight

None of this requires a paid API key, a server you have to manage by hand, or more than about 150 lines of Python. It is the same pattern used by uptime-monitoring tools: Checkly’s public Epic Games availability page, for instance, runs checks where “a check passes when the endpoint returns a status below 400 within 10 seconds,” which is effectively what step 4 below implements for your own purposes.

Prerequisites

Gather these before you start. Version mismatches are the single biggest source of “it worked yesterday” bugs in small polling scripts like this one.

RequirementVersion used in this guideWhy it matters
Python3.11 or newerNative tomllib and faster json handling (3.9 also works with minor tweaks)
requests2.32.xHTTP client for polling both APIs
python-dotenv1.0.xKeeps your Discord webhook URL out of source control
Discord servern/aYou need “Manage Webhooks” permission on one channel
cron or Task Schedulerany current OS buildRuns the script every few minutes without you babysitting it
Disk space< 5 MBState file and rotated logs stay tiny

You do not need a Fortnite account, Epic developer credentials, or the Epic Games Fortnite API documentation portal access for this project. Both endpoints this script calls are public and unauthenticated.

Step 1: Map the two data sources you’ll poll

Before writing code, confirm the shape of the data you’re working with. Epic’s status API exposes a summary endpoint at status.epicgames.com/api/v2/summary.json. According to the official status API documentation, this endpoint returns “an indicator, one of none, minor, major, or critical, as well as a human description of the blended component status,” plus arrays for active incidents and scheduled maintenances. That indicator field is your primary trigger: when it flips from “none,” something is happening to Fortnite’s backend right now.

The second source, Fortnite-API.com, mirrors Epic’s in-game news feed, map data, and playlist info as JSON. Its value for update-detection is that Epic sometimes pushes map or news changes as part of an update without triggering a formal “maintenance” entry on the status page. Hashing that response catches those quieter changes. Fetch both once manually to see the raw shape:

curl -s https://status.epicgames.com/api/v2/summary.json | python3 -m json.tool | head -30
curl -s https://fortnite-api.com/v2/news | python3 -m json.tool | head -30

Run both and keep the output open in a second terminal. You’ll reference the field names (status.indicator, scheduled_maintenances, and the news payload’s data object) throughout the rest of this build.

Step 2: Set up the project and virtual environment

Create an isolated folder so this project’s dependencies never collide with anything else on your machine.

mkdir fn-update-watch && cd fn-update-watch
python3 -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install requests python-dotenv
touch fn_update_watch.py .env state.json

Open .env and add one line, which you’ll fill in during Step 6 once you’ve created the Discord webhook:

DISCORD_WEBHOOK_URL=

Add .env and venv/ to a .gitignore file immediately if you plan to version this project. A leaked webhook URL lets anyone spam your channel. It is not a catastrophic secret, but it’s an annoying one to rotate.

Step 3: Write the status-poller function

This function hits Epic’s summary endpoint and pulls out exactly the fields you need: the overall indicator and the list of scheduled maintenances. Keep it narrow. A common mistake in monitoring scripts is parsing the entire response when you only act on two or three fields.

import requests

EPIC_STATUS_URL = "https://status.epicgames.com/api/v2/summary.json"

def fetch_epic_status(timeout=10):
    resp = requests.get(EPIC_STATUS_URL, timeout=timeout)
    resp.raise_for_status()
    data = resp.json()
    indicator = data.get("status", {}).get("indicator", "unknown")
    maintenances = [
        {"name": m.get("name"), "status": m.get("status")}
        for m in data.get("scheduled_maintenances", [])
    ]
    return {"indicator": indicator, "maintenances": maintenances}

The timeout=10 argument matters more than it looks. Without it, a hung connection to Epic’s CDN can leave your poller stuck indefinitely instead of failing fast and retrying on the next cycle.

Step 4: Add the Fortnite-API.com content-hash check

The status API tells you when Epic declares an incident. The news endpoint tells you when content actually moved, even quietly. Hash the response body so you’re comparing a short fingerprint instead of diffing full JSON blobs on every run.

import hashlib
import requests

NEWS_URL = "https://fortnite-api.com/v2/news"

def fetch_news_hash(timeout=10):
    resp = requests.get(NEWS_URL, timeout=timeout)
    resp.raise_for_status()
    body = resp.content
    return hashlib.sha256(body).hexdigest()

SHA-256 is overkill for collision resistance here (you’re not defending against an adversary, just detecting change), but it’s built into Python’s standard library and fast enough that the extra security margin costs nothing.

Step 5: Persist state between runs

A poller that forgets everything between runs is useless, it would alert on the same unchanged status every single cycle. Store the last known indicator and hash in state.json and compare against it each run.

import json
import os

STATE_FILE = "state.json"

def load_state():
    if not os.path.exists(STATE_FILE):
        return {"indicator": "none", "news_hash": ""}
    with open(STATE_FILE, "r") as f:
        return json.load(f)

def save_state(state):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)

This is the whole trick behind change detection: read the old state, fetch the new state, compare, act only on the difference, then overwrite the old state. Every step after this one builds on that loop.

Step 6: Create the Discord webhook

In Discord, open Server Settings > Integrations > Webhooks, create a new webhook, point it at the channel you want alerts in, and copy the URL. Paste it into your .env file from Step 2. Discord’s own webhook documentation describes this as the standard way to let an external service post messages to a channel using a unique token, which is exactly the job here.

import os
import requests
from dotenv import load_dotenv

load_dotenv()
WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")

def notify_discord(message: str):
    if not WEBHOOK_URL:
        raise RuntimeError("DISCORD_WEBHOOK_URL is not set in .env")
    payload = {"content": message}
    resp = requests.post(WEBHOOK_URL, json=payload, timeout=10)
    resp.raise_for_status()

Test this function by itself before wiring it into the full loop. Run python3 -c "from fn_update_watch import notify_discord; notify_discord('test alert')" and confirm the message lands in your channel. If it doesn’t, fix it now rather than debugging it inside a 150-line script later.

Step 7: Wire the comparison and alert logic together

Now combine Steps 3 through 6 into a single check function. This is the heart of the script: fetch both sources, compare against saved state, and fire exactly one alert per real change.

def run_check():
    state = load_state()
    epic = fetch_epic_status()
    news_hash = fetch_news_hash()

    changed = []
    if epic["indicator"] != state["indicator"]:
        changed.append(
            f"Epic status changed: {state['indicator']} -> {epic['indicator']}"
        )
    if news_hash != state["news_hash"]:
        changed.append("Fortnite news/content payload changed (possible update)")

    if epic["maintenances"]:
        names = ", ".join(m["name"] for m in epic["maintenances"])
        changed.append(f"Scheduled maintenance active: {names}")

    if changed:
        notify_discord("Fortnite update signal detected:\n" + "\n".join(changed))

    state["indicator"] = epic["indicator"]
    state["news_hash"] = news_hash
    save_state(state)

Notice that a scheduled maintenance entry triggers an alert on every run while it’s active, not just once. That’s intentional: it keeps you informed if a maintenance window gets extended, which happened during the September 22 Epic Online Services maintenance when degraded sessions ran for up to 90 minutes.

Step 8: Add retry logic and error handling

Public APIs occasionally time out, rate-limit you, or return a 500 during their own deploys. Wrap the two fetch calls in a retry helper so one bad response doesn’t crash your cron job and silently stop monitoring for hours.

import time

def with_retries(func, attempts=3, base_delay=2):
    last_err = None
    for i in range(attempts):
        try:
            return func()
        except requests.RequestException as e:
            last_err = e
            time.sleep(base_delay * (2 ** i))
    raise last_err

Update run_check() to call with_retries(fetch_epic_status) and with_retries(fetch_news_hash) instead of calling them directly. The exponential backoff (2s, 4s, 8s) gives transient network blips time to clear without hammering either API.

Step 9: Add rotating logs

Add Python’s built-in logging module so you have a record of every poll, not just the ones that triggered an alert. This is what lets you answer “was the watcher even running at 3 a.m.?” without guessing.

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("fn_update_watch")
logger.setLevel(logging.INFO)
handler = RotatingFileHandler("watch.log", maxBytes=1_000_000, backupCount=3)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)

Call logger.info("poll ok, indicator=%s", epic["indicator"]) at the end of every successful run_check() call, and logger.error("poll failed: %s", err) inside a try/except wrapper around the whole function. Three rotated 1 MB files are more than enough history for a script this size.

Step 10: Schedule it with cron

Polling every 5 minutes is frequent enough to catch a maintenance window announcement quickly without abusing either free API. Open your crontab with crontab -e and add:

*/5 * * * * cd /path/to/fn-update-watch && venv/bin/python fn_update_watch.py >> cron.log 2>&1

You can sanity-check any cron expression at crontab.guru before saving it. On Windows, Task Scheduler’s “Create Basic Task” wizard with a 5-minute repeat trigger achieves the same thing without the crontab syntax.

Step 11: Add a self-check so you know the watcher itself is alive

A monitoring script that silently dies is worse than no monitoring at all, because you stop checking manually once you believe it’s covered. Add a daily heartbeat so you’d notice if cron stopped firing.

from datetime import datetime, timezone

def maybe_send_heartbeat(state):
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    if state.get("last_heartbeat") != today:
        notify_discord(f"fn-update-watch heartbeat OK — {today}")
        state["last_heartbeat"] = today

Call this once inside run_check() right before save_state(state). If a day passes with no heartbeat in your channel, you know the cron job, or the whole machine, stopped, independent of whether Fortnite itself had an update.

Step 12: Test it end-to-end with a simulated change

Don’t wait for a real Fortnite update to find out if your alerting path works. Force a false state manually and confirm the full loop fires correctly.

echo '{"indicator": "major", "news_hash": "deadbeef"}' > state.json
python3 fn_update_watch.py
cat watch.log | tail -5

Because the stored indicator (“major”) won’t match the real one Epic returns (almost always “none” outside an incident), and the fake hash won’t match the real news hash, this should trigger two change messages in your Discord channel immediately. If nothing arrives, check your webhook URL and the retry logic before assuming the APIs are down.

Output examples: what a real alert looks like

Here’s what the Discord message looks like when it fires on an actual scheduled maintenance window, modeled on the wording Epic used for the October 1 v42.30 downtime:

Fortnite update signal detected:
Epic status changed: none -> minor
Scheduled maintenance active: Fortnite v42.30 Deployment

And a typical quiet-day log entry when nothing changed:

2026-10-02 14:05:01 INFO poll ok, indicator=none
2026-10-02 14:10:02 INFO poll ok, indicator=none

Epic’s own status wording during an active maintenance window typically reads “Scheduled maintenance is currently in progress. We will provide updates as necessary,” confirming the semantics your indicator field is capturing.

Recent Fortnite update timeline (for reference and testing)

Use this table to sanity-check your watcher’s history against known, verified events from Epic’s status page.

DateEventWindowSource
Sept 3, 2026v42.10 downtime4:00 a.m. ET / 8:00 a.m. UTCEpic status page
Sept 22, 2026Epic Online Services maintenance6:00 a.m. UTC, up to 90 min degradedEpic status page
Oct 1, 2026v42.30 downtime (Fortnitemares 2026 start)4:00 a.m. ET / 8:00 a.m. UTCEpic status page
Oct 15, 2026 (unconfirmed)Projected follow-up patchNot yet announcedThird-party trackers only
Oct 29, 2026 (unconfirmed)Projected follow-up patchNot yet announcedThird-party trackers only

The last two rows exist specifically so you remember the difference between what your watcher will confirm and what a schedule site is merely guessing. Only your own poller, pointed at the real status API, can tell you when October 15 or 29 actually becomes real.

Why this beats watching leakers and Reddit threads

Fortnite has a sizable community of dataminers who pull unreleased cosmetics and map changes out of game files within hours of a patch going live. That’s genuinely useful for previewing content, but it’s a poor substitute for knowing when an update is actually happening. Leak accounts post on their own schedule, not Epic’s, and a post that says “next update incoming” is a guess based on patterns, not a confirmed fact pulled from Epic’s infrastructure. Your watcher removes that guesswork by going straight to the source Epic itself updates.

False positives from vaulted or encrypted content

Datamined build files sometimes reference content that never ships, or ships months later than the leak implied. Epic also encrypts a growing share of unreleased assets specifically to blunt early leaks, which means a leak-based signal can be stale or simply wrong by the time it reaches you. A status-API signal doesn’t have this problem, because it reflects what Epic’s own servers are doing right now, not what a miner thinks is coming.

Why hotfixes slip past leak accounts entirely

Server-side hotfixes, the kind that adjust drop rates or disable a bugged weapon without a client download, rarely produce anything a dataminer can find, since there’s no new build to mine. These changes show up in the news-feed hash your watcher checks in Step 4, which is one of the clearer advantages of polling live data over waiting on community callouts. If your goal is knowing the moment something actually changed in the game, rather than knowing what might ship weeks from now, the API-based approach in this guide gets there faster and with fewer false alarms.

Common pitfalls

Most of the bugs people hit with a script like this aren’t in the polling logic, they’re in the assumptions baked in around it. The list below covers the mistakes that show up most often in small monitoring scripts, Fortnite-specific or otherwise.

  1. Polling too aggressively. Running this every 10 seconds instead of every 5 minutes doesn’t get you earlier alerts, it just risks a rate limit on a free public API that other developers rely on too.
  2. Hard-coding version number patterns. Fortnite skipped v42.20 in public releases between v42.10 and v42.30. Code that assumes sequential numbering will misfire or miss updates entirely.
  3. Forgetting the timeout argument. A requests.get() call with no timeout can hang your whole cron job indefinitely if Epic’s CDN stalls.
  4. Treating third-party schedule sites as confirmed dates. Dates like October 15 or 29 circulating on tracker sites are projections. Alert on the status API’s own data, not on a scraped calendar.
  5. Committing the .env file. A leaked Discord webhook URL means strangers can post to your channel until you regenerate it.
  6. Not testing the alert path before relying on it. Step 12 exists because the worst time to discover a broken webhook is during an actual update window you needed to catch.
  7. Ignoring the heartbeat. Without Step 11, a crashed cron job looks identical to a quiet Fortnite day, silence either way.

Troubleshooting

If the watcher stops behaving the way it did during your Step 12 test, work through these in order before assuming the Fortnite or Epic side is at fault. Most failures trace back to the local environment, not the remote APIs.

  • No Discord message ever arrives. Confirm DISCORD_WEBHOOK_URL is actually loaded by printing os.getenv("DISCORD_WEBHOOK_URL"). A missing .env file in the working directory is the most common cause.
  • Script runs manually but not from cron. Cron often uses a different PATH and working directory than your shell. Always cd into the project folder inside the crontab line, as shown in Step 10.
  • requests.exceptions.ConnectionError on every run. Check whether your host or network blocks outbound HTTPS on a schedule (common on some shared hosting or corporate networks).
  • 429 Too Many Requests from Fortnite-API.com. You’re polling too often. Five-minute intervals are safe. Sub-minute intervals are not.
  • Alerts fire on every single run, not just on change. Your state.json isn’t being written correctly, double-check that save_state() runs after every check, including inside error branches.
  • JSONDecodeError when parsing the news endpoint. The API occasionally returns an HTML error page instead of JSON during its own downtime. Wrap .json() calls in a try/except and log the raw text when it fails.
  • Heartbeat never arrives even though checks run fine. Confirm your server’s clock and timezone are correct. The date comparison in Step 11 uses UTC and will never match if the system clock has drifted.
  • Logs grow unexpectedly large. Confirm RotatingFileHandler‘s maxBytes and backupCount are actually taking effect. A typo that creates a second unrotated log file is an easy miss.

Advanced tips: beyond a single Discord channel

Once the base script runs reliably, a few extensions make it more useful without much extra code. Add a second webhook call for Slack or Telegram and fan out the same changed list to both, so you’re not locked into one platform. If you run a Discord server for a clan or community, route maintenance-window alerts to a quiet “status” channel and reserve a louder “update-live” channel for when the indicator flips back to “none,” which is usually the real signal that new content is playable.

For deployment, a free-tier box on a provider like Oracle Cloud’s Always Free tier or a $5/month VPS both comfortably run this script forever since it uses almost no CPU between polls. If you’d rather not manage a server at all, a scheduled GitHub Actions workflow on a 5-minute cron trigger can run the same script statelessly, storing state.json as a repository artifact between runs.

Finally, consider diffing the actual patch notes text once Epic’s indicator changes, rather than just alerting on the change itself. Fetching the linked patch notes page and running a simple word-count comparison against the previous version tells you at a glance whether an update is a major content drop or a one-line bug fix, without you having to read the full notes every time.

Scaling the same pattern to other live-service games

Nothing about this architecture is Fortnite-specific once you strip away the two URLs. Any live-service game with a public status page and a JSON data mirror can plug into the same load_state()/save_state() loop. If you also play a game with a similar community API, duplicate the fetch functions, point them at the new endpoints, and store each game’s state under its own key in state.json rather than running a separate script per title. That keeps one cron entry, one log file, and one Discord channel doing the work that would otherwise take several copy-pasted scripts to maintain.

The complete working project

Here is the full script with every step from above assembled into one file. Save this as fn_update_watch.py in the project folder you created in Step 2.

import hashlib
import json
import logging
import os
import time
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler

import requests
from dotenv import load_dotenv

load_dotenv()
WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
EPIC_STATUS_URL = "https://status.epicgames.com/api/v2/summary.json"
NEWS_URL = "https://fortnite-api.com/v2/news"
STATE_FILE = "state.json"

logger = logging.getLogger("fn_update_watch")
logger.setLevel(logging.INFO)
handler = RotatingFileHandler("watch.log", maxBytes=1_000_000, backupCount=3)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)


def with_retries(func, attempts=3, base_delay=2):
    last_err = None
    for i in range(attempts):
        try:
            return func()
        except requests.RequestException as e:
            last_err = e
            time.sleep(base_delay * (2 ** i))
    raise last_err


def fetch_epic_status(timeout=10):
    resp = requests.get(EPIC_STATUS_URL, timeout=timeout)
    resp.raise_for_status()
    data = resp.json()
    indicator = data.get("status", {}).get("indicator", "unknown")
    maintenances = [
        {"name": m.get("name"), "status": m.get("status")}
        for m in data.get("scheduled_maintenances", [])
    ]
    return {"indicator": indicator, "maintenances": maintenances}


def fetch_news_hash(timeout=10):
    resp = requests.get(NEWS_URL, timeout=timeout)
    resp.raise_for_status()
    return hashlib.sha256(resp.content).hexdigest()


def load_state():
    if not os.path.exists(STATE_FILE):
        return {"indicator": "none", "news_hash": "", "last_heartbeat": ""}
    with open(STATE_FILE, "r") as f:
        return json.load(f)


def save_state(state):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)


def notify_discord(message: str):
    if not WEBHOOK_URL:
        raise RuntimeError("DISCORD_WEBHOOK_URL is not set in .env")
    resp = requests.post(WEBHOOK_URL, json={"content": message}, timeout=10)
    resp.raise_for_status()


def maybe_send_heartbeat(state):
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    if state.get("last_heartbeat") != today:
        notify_discord(f"fn-update-watch heartbeat OK — {today}")
        state["last_heartbeat"] = today


def run_check():
    state = load_state()
    try:
        epic = with_retries(fetch_epic_status)
        news_hash = with_retries(fetch_news_hash)
    except requests.RequestException as e:
        logger.error("poll failed: %s", e)
        return

    changed = []
    if epic["indicator"] != state["indicator"]:
        changed.append(f"Epic status changed: {state['indicator']} -> {epic['indicator']}")
    if news_hash != state["news_hash"]:
        changed.append("Fortnite news/content payload changed (possible update)")
    if epic["maintenances"]:
        names = ", ".join(m["name"] for m in epic["maintenances"])
        changed.append(f"Scheduled maintenance active: {names}")

    if changed:
        notify_discord("Fortnite update signal detected:\n" + "\n".join(changed))
        logger.info("alert sent: %s", changed)

    state["indicator"] = epic["indicator"]
    state["news_hash"] = news_hash
    maybe_send_heartbeat(state)
    save_state(state)
    logger.info("poll ok, indicator=%s", epic["indicator"])


if __name__ == "__main__":
    run_check()

Run it once with python3 fn_update_watch.py to confirm it executes cleanly, then let cron take over on the five-minute schedule from Step 10. Keep the script in version control even if you never publish it anywhere, a local git repository with regular commits makes it trivial to see exactly what changed if a future edit breaks the alert logic months from now.

One last detail worth confirming before you walk away from this: make sure state.json and watch.log are writable by whatever user cron runs the job as. It’s a small thing, but a permissions mismatch between your interactive shell user and cron’s execution user is a surprisingly common reason a script that “worked when I tested it” goes quiet the moment it’s scheduled.

Polling interval tradeoffs

Picking the right interval balances how fast you want to know against how much load you put on free public infrastructure that other developers share.

IntervalMax alert delayRequests/dayRecommended use case
1 minute~1 min2,880Not recommended, risks rate limiting on free APIs
5 minutes~5 min576Default for this tutorial, good balance
15 minutes~15 min192Low-traffic personal use, community Discord bots
60 minutes~60 min48Casual monitoring, not time-sensitive alerts

Five minutes is a reasonable default for most readers. If you run this watcher for a large Discord community where members expect near-instant notice, 2-3 minutes is still safe, just keep an eye on your logs for any 429 responses and back off if you see them.

Bonus: pulling Epic’s incident history for context

Epic’s status site also exposes an incident history page, which the status documentation describes as providing prior incidents, including past Fortnite maintenance records. This is useful once your watcher has been running for a while and you want to answer a question like “how often does Epic actually hit its stated maintenance window.” The history data isn’t needed for real-time alerting, but it’s a handy add-on once the core script in this guide is stable.

import requests

HISTORY_URL = "https://status.epicgames.com/api/v2/incidents.json"

def fetch_recent_incidents(limit=5, timeout=10):
    resp = requests.get(HISTORY_URL, timeout=timeout)
    resp.raise_for_status()
    incidents = resp.json().get("incidents", [])[:limit]
    return [
        {"name": i.get("name"), "status": i.get("status"), "created_at": i.get("created_at")}
        for i in incidents
    ]

Wire this into a separate, lower-frequency cron entry, once an hour is plenty, and post a short weekly digest to Discord instead of alerting on every entry. That gives your community a running record of actual Epic reliability over time, built entirely from the same public endpoint family you’re already polling for live alerts, without turning a quiet historical log into another source of alert fatigue.

Frequently asked questions

Does Epic Games publish an official Fortnite patch calendar?
No. Epic announces maintenance windows individually through the status page shortly before they happen, usually with 30 minutes to a few hours of notice. There is no forward-looking calendar of future patch dates.

Is Fortnite-API.com an official Epic Games service?
No, it’s a community-run project that mirrors Epic’s public in-game data as JSON. It’s reliable for this kind of monitoring but isn’t operated by Epic itself.

Why did my watcher alert on a maintenance that wasn’t about Fortnite?
Epic’s status summary endpoint covers multiple Epic Games Store and Epic Online Services components, not just Fortnite. Check the maintenances name field in your alert to confirm it’s Fortnite-specific before assuming it affects your game session.

Can I run this without a Discord server?
Yes. Swap notify_discord() for any webhook-based service, Slack’s incoming webhooks and Telegram’s Bot API both use nearly identical JSON POST patterns.

How accurate are third-party sites that predict exact future update dates?
Mixed. They’re often right about the general cadence (roughly every two to three weeks) but wrong about exact dates, since those depend on Epic’s internal QA timeline. Treat unconfirmed dates as estimates, not facts, until the status API shows real maintenance scheduled.

Will this script get my IP rate-limited or banned?
Not at the 5-minute interval used in this guide. Both APIs are designed for exactly this kind of lightweight polling. The risk only appears if you poll far more frequently than needed.

Do I need an Epic Games developer account to use the status API?
No. The status summary endpoint is public and unauthenticated, no API key or developer registration required.

What’s the difference between a hotfix and a numbered update like v42.30?
A numbered update ships through the full build and store-approval pipeline and almost always requires a client restart and matchmaking downtime. A hotfix changes server-side values (drop rates, playlist settings) without requiring you to download anything, and it typically won’t appear as a version bump at all.