You deploy a little Streamlit app on Community Cloud, share the link, and a day later it's showing a sleepy "Zzzz, this app has gone to sleep due to inactivity" page. Anyone who opens it has to click a button and wait for a cold start.
There's no keep-alive switch in Streamlit Community Cloud, and the advice on the forums boils down to "push an empty commit" or "automate a browser". Both work, but both are heavier than they need to be. There's a simpler way: do exactly what that button does, with one HTTP request, on a schedule.
⚠️ This uses an undocumented endpoint that the Streamlit web page itself calls. It can change without notice. Only use it on apps you own, and keep the schedule gentle.
What the "wake up" button really does
If you open dev tools on a sleeping app and click Yes, get this app back up!, you'll see a single request:
POST https://<your-app>.streamlit.app/api/v2/app/resume
It needs two small things besides the POST itself, and neither one is a login:
-
A CSRF token. The page never stores it anywhere fancy. It simply remembers the
x-csrf-tokenresponse header from the previous API call and sends it back. The server also sets a matching_streamlit_csrfcookie on that same response. -
A machine id. A random UUID sent as
x-streamlit-machine-id.
So the whole dance is: ask for the app status, grab the token and cookie, then POST to resume.
Try it with curl
I'm using webpie.streamlit.app here, swap in your own app URL. Save this as wake.sh:
#!/usr/bin/env bash
# Wake a sleeping Streamlit Community Cloud app.
# Usage: ./wake.sh https://webpie.streamlit.app
set -euo pipefail
APP_URL="${1:?usage: ./wake.sh https://webpie.streamlit.app}"
COOKIES="$(mktemp)"
trap 'rm -f "$COOKIES"' EXIT
# Step 1: any API call hands us a CSRF cookie (saved in the jar)
# and an x-csrf-token response header (we grab it here).
CSRF_TOKEN=$(
curl --silent --output /dev/null --dump-header - \
--cookie-jar "$COOKIES" \
"$APP_URL/api/v2/app/status" \
| tr -d '\r' \
| awk -F': ' 'tolower($1) == "x-csrf-token" { print $2 }'
)
# Step 2: the browser also sends a random UUID as a machine id.
MACHINE_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
# Step 3: this is the call behind the "Yes, get this app back up!" button.
curl --silent --include --request POST \
--cookie "$COOKIES" --cookie-jar "$COOKIES" \
--header "x-csrf-token: $CSRF_TOKEN" \
--header "x-streamlit-machine-id: $MACHINE_ID" \
--header "origin: $APP_URL" \
--header "referer: $APP_URL/" \
"$APP_URL/api/v2/app/resume"
echo
# Step 4 (optional): check the status afterwards
curl --silent --cookie "$COOKIES" "$APP_URL/api/v2/app/status"
echo
Run it:
chmod +x wake.sh
./wake.sh https://webpie.streamlit.app
A 204 No Content from the resume call means the request was accepted. The status call at the end shows the app's state; while it's booting, the status number changes. (An app that's already awake just answers normally, so it's safe to run any time.)
Make it run itself
A shell script on your laptop doesn't help while you're asleep too. The nice part is that the logic is just fetch calls, so it runs on almost any serverless platform: Cloudflare Workers, Vercel, Netlify, AWS Lambda, Deno Deploy. Pick whichever you already use.
Here's the core logic once, in plain JavaScript:
// Shared logic: wake a Streamlit Community Cloud app.
// Works in Cloudflare Workers, Vercel, Netlify, Node 18+, anything with fetch.
export async function wakeApp(appUrl) {
// Step 1: any API call gives us a CSRF cookie and an x-csrf-token header.
const status = await fetch(`${appUrl}/api/v2/app/status`);
const csrfToken = status.headers.get("x-csrf-token");
const cookies = status.headers
.getSetCookie()
.map((cookie) => cookie.split(";")[0]) // keep "name=value", drop attributes
.join("; ");
// Step 2: this is the call behind the "Yes, get this app back up!" button.
const resume = await fetch(`${appUrl}/api/v2/app/resume`, {
method: "POST",
headers: {
cookie: cookies,
"x-csrf-token": csrfToken,
"x-streamlit-machine-id": crypto.randomUUID(),
origin: appUrl,
referer: `${appUrl}/`,
},
});
return { ok: resume.ok, status: resume.status };
}
Option A: Cloudflare Workers
worker.js:
import { wakeApp } from "./wake.js";
export default {
async fetch(request, env) {
// Optional: stop strangers from poking your endpoint
const secret = new URL(request.url).searchParams.get("key");
if (env.WAKE_KEY && secret !== env.WAKE_KEY) {
return new Response("Nope", { status: 401 });
}
const result = await wakeApp(env.APP_URL);
return Response.json(result);
},
};
Set APP_URL (for example https://webpie.streamlit.app) and an optional WAKE_KEY as variables in the Worker settings, then deploy with npx wrangler deploy. You'll get a URL like https://wake-streamlit.<you>.workers.dev/?key=<your key>.
Option B: Vercel
Put wake.js in your project root and add api/wake.js:
import { wakeApp } from "../wake.js";
export default async function handler(req, res) {
if (process.env.WAKE_KEY && req.query.key !== process.env.WAKE_KEY) {
return res.status(401).send("Nope");
}
const result = await wakeApp(process.env.APP_URL);
res.status(200).json(result);
}
Add APP_URL and WAKE_KEY under Project Settings → Environment Variables and deploy. Your endpoint is https://<project>.vercel.app/api/wake?key=<your key>.
Trigger it with cron-job.org (free)
You don't even need the platform's own scheduler. cron-job.org is free and will call any URL on a schedule:
- Create an account and click Create cronjob.
-
URL: the Worker or Vercel endpoint from above, including
?key=.... - Schedule: every 6 hours is plenty. Apps seem to sleep after roughly 12 hours without visitors, so you want to land well inside that window.
- Save. Under the job's history you can see each run's status code.
200means your function ran.
That's the whole setup: cron-job.org pokes your serverless function, the function pokes Streamlit, and your app stays warm. No browser, no Selenium, no fake commits.
Things worth knowing
- It's unofficial. If Streamlit changes the endpoint, the script breaks. Check the cron history now and then.
- It's polite use only. One request every few hours for your own app. Please don't hammer it.
- Waking isn't the same as being busy. This resumes a sleeping app. It doesn't pretend to be traffic, so the app can still go back to sleep later, which is why the schedule keeps repeating.
-
Keep the
keysecret so nobody else can trigger your function.
If you'd rather stay inside the official lines, the alternative is hosting the app somewhere that doesn't sleep. But for a free-tier hobby app, a 20-line function and a free cron service is hard to beat.
Happy hacking!
Top comments (0)