learn

Cold starts and keep-alive

Why free services sleep, the hours math, and how to ping them awake.

Render spins a free web service down after 15 minutes without inbound traffic, and waking takes roughly a minute. Each workspace gets 750 free instance hours a month, shared by all free web services; spun-down services use no hours, and exceeding the allowance suspends every free service until next month. Render may also restart a free service at any time.

The hours math

Hours equal services times hours per day times days:

Services awakeHours/dayDaysHours/month
22431~1,488 — over the 750 limit
12431~744 — leaves nothing for others
2830~480 — fits with room to spare

Keeping both services awake around the clock is not possible on free. Ping only inside a daily window instead — 8 hours a day for both services costs about 480 hours a month. Ping every 10 minutes, which sits safely under the 15-minute idle limit. Never ping /robots.txt: while a service is spun down, Render answers that path itself and the service never wakes.

Target the HTTP service's /health. For the WS service, ping its public HTTP URL — but know what it does: the WS server answers only WebSocket upgrades, so a plain GET gets no application response (checked against the code with a local bare-server test). The inbound connection still counts as traffic; confirm the service really stays awake in the Render dashboard during your window.

Option 1: Cloudflare Worker with a Cron Trigger

Free and precise. The Worker fetches both URLs on a schedule; both URLs come from env vars.

export default {
  // The WS URL never answers — its server only speaks WebSocket
  // upgrades — so each fetch gets a guard; the connection itself
  // is what counts as traffic.
  async scheduled(event, env, ctx) {
    const urls = [env.HTTP_HEALTH_URL, env.WS_PUBLIC_URL].filter(Boolean);
    const ping = (url) =>
      fetch(url, { signal: AbortSignal.timeout(30_000) }).catch(() => {});
    ctx.waitUntil(Promise.all(urls.map(ping)));
  },
};
{
  "name": "render-keepalive",
  "main": "src/index.js",
  "compatibility_date": "2026-10-11",
  "triggers": {
    // Every 10 minutes, 04:00–12:00 UTC (an 8-hour window).
    "crons": ["*/10 4-11 * * *"]
  },
  "vars": {
    "HTTP_HEALTH_URL": "https://<http-service>.onrender.com/health",
    "WS_PUBLIC_URL": "https://<ws-service>.onrender.com/"
  }
}

Convert the window to UTC.

Cron Triggers run in UTC. Subtract your offset from both ends of your 8-hour window — 08:00–16:00 at UTC+2 becomes 06:00–14:00 UTC, written as `*/10 6-13

    • *`. Half-hour zones should round the window out to whole hours, and zones with daylight saving need the cron adjusted twice a year.

Deploy with Wrangler and check the first runs.

Trigger changes can take several minutes to propagate. Watch the Worker's cron events, then confirm both Render services show traffic in the dashboard during the window.

The Workers free plan allows 100,000 requests a day — a few hundred pings barely register. Cron triggers per account are capped at a small number, so one Worker pinging both URLs is the right shape.

What pinging cannot fix

The flush worker is a continuously running BullMQ process, not an HTTP service, so pinging does not run it. While it is not running, flushes are delayed or lost and room state sits in Redis only. Free Redis can also lose data on restart, which is why this guide uses Upstash over TCP instead of Render Key Value.

Option 2: cron-job.org

Create one job per URL.

One job fetches the HTTP /health URL, another the WS public URL, each every 10 minutes.

Set active hours in your own timezone and turn on failure notifications.

Expect the WS job to report failures — that URL never answers, as explained above — so judge health from the Render dashboard, not the WS job status.

Option 3: Monitors or GitHub Actions

UptimeRobot-style monitors work the same way: ping /health every 10 minutes inside the window. A GitHub Actions schedule can also curl both URLs, but scheduled runs are not time-precise, so keep the 10-minute cadence with margin.

Stay within the terms

Free tiers are for demos and hobbies. Ping the smallest window you need, and check each provider's terms rather than stretching them.

Last verified: 2026-10-11. Free-tier limits change — check the providers before relying on any number.

Provider docs

What it owns

  • The free-tier sleep and hour limits, stated plainly
  • A daily ping window that fits the allowance
  • Three keep-alive options with steps
Keep-alive pings
BrowserAPIRealtimeCacheDatabaseShareddurableephemeral

Talks to

Sources: apps/http-server/src/app.ts, apps/http-server/src/index.ts, apps/ws-server/src/server.ts, apps/ws-server/src/index.ts, apps/flush-worker/src/index.ts, packages/redis/src/index.ts (documented at commit b0026d9)

On this page