Back to blog
By Hidde Kehrer7 min read

How to run a Python script 24/7

You have a script. A scraper, a price watcher, a bot, a queue consumer, something that polls an API and writes to SQLite. It works on your laptop. Now it has to keep working when the laptop is closed.

This is a small problem with a lot of bad answers, so here is the short version. You need somewhere that stays powered on, something that restarts the script when it dies, and somewhere for its state to live. Then you need to not think about it again.

Why the obvious options fall over

Your laptop. It sleeps, it reboots for updates, it goes in a bag. A process started in a terminal dies when the terminal closes. nohup python bot.py & survives the terminal but not a reboot, and does nothing when the script crashes at 4am. tmux is the same story with a nicer interface. Both are fine for a job you are watching and wrong for a job you are relying on.

GitHub Actions on a cron schedule. Free, already set up, and genuinely reasonable for a job that runs once a day and writes its output to a repo. Three limits arrive quickly. The schedule event runs at most once every five minutes, so anything more frequent is out. Each run starts from nothing, so there is no state between runs unless you commit it or push it somewhere. And in a public repository, "scheduled workflows are automatically disabled when no repository activity has occurred in 60 days." A scraper that silently stopped two months ago is worse than one that never started, because you stopped checking.

Serverless functions. AWS Lambda has a hard function timeout of 900 seconds. Vercel Functions cap at 300 seconds on Hobby, 800 on Pro. If your script finishes in under a minute and keeps no state, this works well. A long-poll bot, a scraper walking 400 pages, or anything holding a websocket does not fit the shape.

A VPS you already have. This is the correct shape and it is what the rest of the article describes. The two costs people underestimate are that you now operate a Linux box, and that you pay for it around the clock whether the script is doing anything or not.

Runs longer than 15 minKeeps state between runsRestarts on crashSurvives a rebootCost
Laptop + nohup / tmuxYesYesNoNoFree
GitHub Actions cronNo, and 5 min is the shortest intervalNoNext run onlyn/aFree, but auto-disabled after 60 days idle in public repos
Serverless functionNo (900 s Lambda, 300 s Vercel Hobby)NoNext invocation onlyn/aNear zero for short jobs
Linux machine + systemdYesYesYesYesCost of the machine

The mechanics: a machine and a systemd unit

Whatever machine you end up on, this is the part that actually makes a script run 24/7, and it is worth doing properly once.

Put the script somewhere sensible and give it its own virtualenv:

mkdir -p ~/pricebot && cd ~/pricebot
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Then write a systemd service. This is the whole difference between a script that runs and a script that stays running:

# /etc/systemd/system/pricebot.service
[Unit]
Description=Price watcher
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=boxd
WorkingDirectory=/home/boxd/pricebot
ExecStart=/home/boxd/pricebot/.venv/bin/python -u main.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=/home/boxd/pricebot/.env

[Install]
WantedBy=multi-user.target

Enable it and it will start on boot and restart on crash:

sudo systemctl daemon-reload
sudo systemctl enable --now pricebot
systemctl status pricebot
journalctl -u pricebot -f      # live logs

Restart=always with RestartSec=5 is the line doing the work. Your script can crash on a bad response at 3am and be back five seconds later. -u and PYTHONUNBUFFERED=1 stop Python buffering its output so journalctl shows you logs as they happen rather than in 4 KB blocks.

For a scheduled job rather than a continuous one, use a systemd timer or plain cron on the same machine. Both do sub-five-minute intervals without complaint, and neither turns itself off after 60 days.

crontab -e
# every 2 minutes
*/2 * * * * /home/boxd/pricebot/.venv/bin/python /home/boxd/pricebot/check.py >> /home/boxd/pricebot/cron.log 2>&1

Doing it on boxd

A boxd machine is a full Ubuntu 24.04 KVM virtual machine with root, 2 vCPU, 8 GB RAM and 100 GB of persistent disk. Ten of them are €20/month. Python 3 with pip, uv and pipx is preinstalled, along with Docker, git and sqlite3. Node.js is not preinstalled, so apt install or nvm if your script is JavaScript.

Create a machine and get into it:

boxd machine new pricebot
ssh -t boxd.sh connect pricebot

Copy the script up from your laptop with the CLI, or just git clone it from inside the machine:

boxd cp ./pricebot pricebot:/home/boxd/pricebot -r

Then the systemd unit above, unchanged. That is the whole procedure. The machine keeps running after you disconnect, and the restart policy is set to always by default, so a reboot brings your service back with it.

Two properties matter for this particular job.

State stays on the disk. SQLite, a dedup set, a cache directory, a token file in ~/.config. It is an ordinary filesystem, so the ordinary answers work and nothing has to become a managed service.

One script per machine is affordable. Ten machines on a flat €20/month means the scraper, the Discord bot and the thing you are still debugging do not have to share a box. Each is a separate virtual machine with its own kernel, so the one that leaks memory cannot take the others down. Worth being straight about the arithmetic: €2 a machine is €20 divided by ten, and one machine on its own still costs €20.

If your script serves HTTP rather than just polling, every machine has an HTTPS URL at <name>.boxd.sh with a certificate handled for you, and you can point it at whatever port you are listening on:

boxd proxy new hooks --vm=pricebot --port=8080
# https://hooks.pricebot.boxd.sh

That is usually the difference between a webhook receiver that works and one where you are running ngrok in another window.

Three shapes, three answers

A loop that runs forever. Bots, websocket consumers, queue workers, anything holding a connection. Systemd service with Restart=always, as above. Do not use cron for this; you will end up with fourteen copies running.

A job on a schedule. Cron or a systemd timer on the machine. If it runs less often than every five minutes, keeps no state, and its output belongs in a repo, GitHub Actions is genuinely fine and free, and you should use that instead of paying for anything.

A thing that waits for a request. A webhook receiver, a small API, a Telegram bot on webhooks rather than long polling. This needs a stable public address with TLS more than it needs raw compute, so pick on reachability.

The check that saves you later

Whatever you build, add one line that tells you when it has stopped. A dead cron job is silent by design, and the failure mode of this entire category is discovering in November that something broke in August.

The cheapest version is a heartbeat: have the script ping a free monitor such as Healthchecks.io or Uptime Kuma on each successful run, and let it email you when the ping stops. Ten minutes of setup, and it turns a silent failure into a notification.

Related reading

Boot a machine, write the unit file, walk away. SSH in whenever you want to see what it has been doing.


Last verified: 2026-07-30. Platform limits change; send corrections to hello@boxd.sh.

Frequently asked

How do I run a Python script 24/7?
Put it on a machine that stays powered on and supervise it with systemd. A unit file with `Restart=always` and `RestartSec=5` starts the script on boot and brings it back within seconds of a crash, and `journalctl -u yourservice -f` gives you logs. `nohup` and `tmux` are not substitutes: neither survives a reboot and neither restarts a process that died.
Can I use GitHub Actions to run a script continuously?
Only for scheduled jobs, and with three limits. The `schedule` event runs at most once every five minutes, every run starts from nothing so there is no state between runs, and in a public repository scheduled workflows are disabled automatically after 60 days with no repository activity. For a once-a-day job whose output belongs in a repo it is genuinely fine and free.
Why can't I run a long script on serverless?
There is a hard timeout. AWS Lambda stops a function at 900 seconds and Vercel Functions cap at 300 seconds on Hobby, 800 on Pro. There is also no persistent disk between invocations. A long-poll bot, a scraper walking hundreds of pages, or anything holding a websocket does not fit that shape.
What is the cheapest way to keep a script running?
If it runs less often than every five minutes, keeps no state and its output belongs in a repo, GitHub Actions cron is free and you should use that. Otherwise it needs a machine. A Hetzner CX23 is €5.49/month; boxd is €20/month for ten machines, which matters once you have more than a couple of scripts and would rather not colocate them.
How do I know if my script has stopped?
Add a heartbeat. Have the script ping a monitor such as Healthchecks.io or a self-hosted Uptime Kuma on each successful run, and let it notify you when the pings stop. A dead cron job is silent by design, and the usual failure mode of this whole category is discovering in November that something broke in August.

Read next