How to keep free-tier apps alive with external cron

Free tiers on Render, Railway, and Supabase have one thing in common: if nobody talks to the service, it goes to sleep. The fix has been known for years: a regular HTTP ping from outside. But each platform has its own sleep timer, its own limits, and its own ways to break your keep-alive while you think everything is working.

Which free tiers still exist in 2026

The number of PaaS platforms offering free tiers has dropped fast. Heroku killed theirs in November 2022. Fly.io followed in October 2024. Koyeb moved free accounts behind a $29/month paywall in February 2026. Glitch shut down entirely. Three options remain.

Render

750 free hours per month. One always-on service uses about 720 of those (24 hours times 30 days), leaving almost nothing for a second app. Free services spin down after 15 minutes without traffic. The first request after sleep takes 30-60 seconds while the container restarts.

Railway

$5 of compute credit per month instead of a fixed timer. Your app runs until the money runs out. A small Node.js or Python app costs about $0.01/hour, so $5 lasts roughly 500 hours. That is about 20 days, not a full month.

Supabase

Pauses free-tier projects after 7 days without a single API request. When it pauses, your PostgreSQL database goes fully offline. Not slow. Offline. The first query after a pause takes 30-90 seconds while the database restarts. Any API request resets the 7-day timer, even a simple read.

Different platforms, different timers, same trick to keep them alive: a regular HTTP request from outside.

Why the ping must come from outside

Most people try the obvious thing first: add a setInterval inside the app that pings itself every few minutes. It does not work. When the platform puts your process to sleep, it kills everything running inside it, including your timer. The app cannot wake itself because the code that should do it is asleep too.

// This does NOT work on Render/Railway free tier
// When the app sleeps, setInterval dies with it
setInterval(() => {
  fetch('https://my-app.onrender.com/healthz');
}, 5 * 60 * 1000);

The ping has to come from a different machine. A cron job on your laptop works if the laptop is always on (it usually is not). GitHub Actions works but has timing issues we will cover below. The reliable option is a dedicated external cron service.

Render keep-alive: the 15-minute timer and the 750-hour budget

Render sleeps free web services after 15 minutes without an inbound request. To prevent that, ping it at least every 14 minutes. Most people go with 10 for safety. You can use a cron expression builder to get the syntax right.

# External cron schedule for Render keep-alive
# Every 10 minutes, hit a lightweight endpoint
*/10 * * * * curl -s https://your-app.onrender.com/healthz > /dev/null

Add a /healthz or /ping endpoint to your app that returns a 200 with minimal work. Pinging your homepage works but wastes resources since it renders templates, queries the database, loads assets. A simple {"status":"ok"} response is enough to reset the timer.

Now the math that nobody writes about. Render gives you 750 free hours per month. One service running 24/7 burns 720 (some months 744). That leaves 6-30 hours for anything else. Two web services need 1,440 hours. You have 750. Keeping both alive around the clock is not possible on the free tier.

One service works. Two means picking which one matters more, or letting the second one sleep at night.

Railway keep-alive: credit-based, not timer-based

Railway does not have a fixed sleep timer. You get $5 of credit and your app runs until it is gone. At $0.01/hour, that is about 500 hours, or 20 days of always-on uptime.

Here is a detail that catches people off guard. Every cold start burns extra CPU during initialization, which eats credit faster than normal running. If your app sleeps and wakes 50 times a day, those startup spikes add up. A steady ping every 10 minutes often costs less credit than repeated sleep-wake cycles, because you skip the CPU spike of each cold boot.

# Railway keep-alive (same pattern, different math)
*/10 * * * * curl -s https://your-app.up.railway.app/healthz > /dev/null

Watch your usage in Railway's dashboard. If $5 runs out by the 20th, reduce the ping frequency or accept downtime for the rest of the month. There is no way to top up free credit.

Supabase keep-alive: one ping per week is enough

Supabase pauses free databases after 7 days without any API request. Much less aggressive than Render's 15 minutes. One request per week keeps the project alive. Most people set a daily ping for extra margin.

# Weekly Supabase keep-alive (Sundays at midnight UTC)
0 0 * * 0 curl -s "https://YOUR_PROJECT_REF.supabase.co/rest/v1/" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY" > /dev/null

The request needs to go through the Supabase REST API with a valid API key. A GET to /rest/v1/ with your anon key in the headers is enough. You can also call an Edge Function. Opening the Supabase Studio dashboard in your browser does not count as activity.

One important difference from Render: when Supabase pauses, the database goes fully offline. Not slow. Down. Your app gets errors instead of data, and users see broken pages until the restart finishes.

Why GitHub Actions is unreliable for this

GitHub Actions supports cron schedules in workflows. Lots of tutorials recommend it for keep-alive. The catch: GitHub does not guarantee timing. A job set to */10 * * * * might run 15, 25, or even 60 minutes late when the platform is busy. Their own docs say scheduled workflows can be delayed or skipped during peak times.

# .github/workflows/keep-alive.yml
# Works most of the time, fails when it matters most
name: Keep Alive
on:
  schedule:
    - cron: '*/10 * * * *'
jobs:
  ping:
    runs-on: ubuntu-latest
    steps:
      - run: curl -s https://your-app.onrender.com/healthz

For Supabase this is fine. A 30-minute delay on a weekly ping does not matter. For Render, it defeats the purpose. A 25-minute gap means your app sleeps, the late ping wakes it, and the next user still waits through a cold start.

UptimeRobot and monitoring tools as keep-alive

UptimeRobot's free plan checks 50 URLs every 5 minutes. Many developers use it as a keep-alive: add your Render URL, set 5-minute checks, done. For simple cases it works well.

The limit: UptimeRobot only sends GET requests. No custom headers, no POST, no authentication. So it works for Render (plain GET to a public URL) but not for Supabase (which needs an API key in the headers).

There is also a design mismatch. Uptime monitors are built to tell you when something is down, not to schedule tasks or send custom requests. For one public GET endpoint, they are great. For anything more complex, you need a proper cron tool.

Setting up keep-alive with WatchCron Cloud Cron

GitHub Actions is unreliable, UptimeRobot cannot send headers, a laptop cron dies when you close the lid. A cloud cron service solves all three: it runs scheduled HTTP requests without your own server, supports custom headers and methods, and fires on time every time. WatchCron's Cloud Cron adds one thing the others do not: it monitors every response and alerts you if the ping starts failing.

Setup takes about two minutes per platform:

For Render, create a Cloud Cron job pointed at https://your-app.onrender.com/healthz, every 10 minutes, GET method. No headers needed.

For Railway, same setup. https://your-app.up.railway.app/healthz, every 10 minutes. Watch credit usage the first week.

For Supabase, point at https://YOUR_PROJECT_REF.supabase.co/rest/v1/, add apikey and Authorization headers with your anon key. Once a day is plenty.

Running both Render and Supabase (a common combo for side projects)? Two Cloud Cron jobs: one every 10 minutes for Render, one daily for Supabase.

What happens when the keep-alive itself fails

This is what every other guide skips. You set up the ping, it works for weeks, then it quietly stops. Maybe the cron service had an outage. Maybe you changed the app URL during a redeploy and forgot to update the ping. Maybe a bad deploy broke the health endpoint. Your app sleeps and you find out when someone complains about 30-second load times.

A basic cron tool like cron-job.org does not tell you when it fails. It fires, gets a 500 or a timeout, and moves on. Ten minutes later it fires again, gets another 500. The cycle continues in silence.

WatchCron's Cloud Cron checks every response. If the health endpoint returns a non-200 status or times out, you get an alert through Slack, email, Discord, or whichever channel you use. You know the keep-alive broke before your users notice. That is the difference between "sends pings" and "sends pings and watches them."

For a weekend project, maybe that is more than you need. For anything other people rely on, knowing when keep-alive stops working is worth the two minutes to set it up.

A health endpoint that actually checks something

Returning {"status":"ok"} from /healthz keeps the platform awake but tells you nothing about the app's real health. The database connection could be dead, an environment variable could be missing, a third-party API could be down. The endpoint returns 200 regardless.

A smarter approach: check at least one real dependency before returning 200.

// Express.js -- checks DB before returning 200
app.get('/healthz', async (req, res) => {
  try {
    await db.query('SELECT 1');
    res.json({ status: 'ok', db: 'connected' });
  } catch (err) {
    res.status(503).json({ status: 'error', db: 'unreachable' });
  }
});
# Flask -- same idea
@app.route('/healthz')
def health():
    try:
        db.session.execute(text('SELECT 1'))
        return jsonify(status='ok', db='connected'), 200
    except Exception:
        return jsonify(status='error', db='unreachable'), 503

When it returns 503, your cron service catches the non-200 response and alerts you. Two things happen at once: the platform stays awake (any response resets the idle timer, even a 503) and you know the app is up but broken.

Free-tier keep-alive limitations

Keep-alive pings solve idle sleep. They do not solve everything else.

Render's 750-hour budget is a hard cap. One service 24/7 nearly exhausts it. Two services cannot both stay alive all month.

Railway's $5 does not stretch to 30 days for most apps. You are buying time, not unlimited hosting.

Supabase caps free storage at 500 MB and limits you to 2 projects. The ping only prevents the pause. It does not change those limits.

Some platforms could shut down accounts that use automated pings to bypass sleep policies. Render does not explicitly ban the practice (their employees have acknowledged it in community forums), but it is a tolerance, not a guarantee. If the project outgrows the free tier, a paid instance is the right move.

Free hosting is for prototypes, demos, and side projects with light traffic. When real users and real money are involved, $7/month for an always-on instance costs less than explaining cold-start delays to every visitor.

Frequently asked

Every 10-14 minutes. Render sleeps free services after 15 minutes of inactivity. A 10-minute interval gives a 5-minute buffer for network delays or missed pings.

One daily or weekly request is negligible. Supabase free tier allows 500 requests per second. A single keep-alive ping is invisible in your usage.

For simple GET requests to public URLs, yes. UptimeRobot checks every 5 minutes on the free plan, enough for Render. It cannot send custom headers, so for Supabase you need an external cron service with header support.

For Supabase (weekly ping), yes. For Render (every 10 minutes), no. GitHub Actions can delay scheduled jobs by 15-60 minutes during peak load, longer than Render's 15-minute sleep timer.

Your app sleeps and the first user request takes 30-90 seconds (or fails entirely for Supabase). Most free cron tools do not alert on failures. WatchCron checks each response and alerts you when the endpoint stops returning 200.