← All guides Running things · 13 min

Where to run a bot, by what the bot does

Hosting advice is usually written for web apps. Bots have different shapes — some wake once a day, some listen constantly, some need a browser — and each shape has a different right answer.

"It works on my laptop" is where every bot starts and where most of them quietly die, usually the first time the lid closes. Getting it running somewhere else is the step between a script and a tool.

The right host depends almost entirely on one question: does your bot wake up, or does it stay awake?

The four shapes

Scheduled. Runs at 7am, or hourly, then exits. Digests, scrapers, reports, backups. The most common shape and the cheapest to host.

Always listening. Holds a connection open waiting for events — a Discord bot, a Slack bot, anything reacting the moment something happens.

Request-driven. Sits idle until something calls it: a webhook, a chat widget's backend, an API. Traffic is spiky and mostly zero.

Heavy or physical. Drives a browser, processes video, or is attached to a sensor in your kitchen. Needs real resources or a specific location.

Scheduled bots

GitHub Actions — the default, and usually free

A scheduled workflow is genuinely excellent for this: versioned with your code, secrets managed for you, logs kept, nothing to maintain. Public repositories get standard runners free; private ones draw on an account allowance — 2,000 minutes a month on Free, 3,000 on Pro at the time of writing. A daily scraper taking two minutes uses about sixty of those.

name: digest
on:
  schedule:
    - cron: "0 7 * * *"     # UTC, always
  workflow_dispatch:         # lets you run it by hand too
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: {python-version: "3.12"}
      - run: pip install -r requirements.txt
      - run: python digest.py
        env:
          API_KEY: ${{ secrets.API_KEY }}

Three things to know before relying on it. Schedules run in UTC, so your 7am job drifts by an hour when the clocks change — if local time matters, schedule both and exit early in the wrong one. Scheduled runs are queued on shared infrastructure and can be late by several minutes, which is fine for a digest and not fine for anything time-critical. And the filesystem is fresh every run: any SQLite file your bot writes is gone unless you commit it back or use the Actions cache. That last one silently breaks every change-detecting bot — every run looks like the first, so everything looks new.

cron on a machine you own

A Raspberry Pi in a drawer, or an old laptop. Free, fully under your control, and state persists — which makes it strictly better than Actions for anything keeping a database.

0 7 * * * cd /home/pi/bot && /usr/bin/python3 digest.py >> bot.log 2>&1

⚠ Cron does not inherit your shell environment. Your API keys won't be there and the job fails silently. Put the variables in the crontab itself or source them in a wrapper script — and always redirect output to a log, or you'll never know it broke.

systemd timers — cron with better manners

On any modern Linux box, timers give you what cron lacks: proper logs via journalctl, automatic retry, dependency ordering, and Persistent=true, which runs a missed job when the machine comes back rather than skipping it. More verbose to set up; better to operate.

Always-listening bots

These need a process that stays up, which rules out the free scheduled options.

OptionRoughlyNotes
Raspberry Pi at home~$2/yr powerCheapest real answer. Needs your internet to stay up.
VPS (Hetzner, DigitalOcean, Vultr)$4–7/moThe honest default. A small instance runs several bots at once.
RenderFree tier existsFree web services spin down after ~15 min idle and cold-start. Fine for webhooks, fatal for a listener.
Railway$5/moNo free tier now — a one-off trial credit, then Hobby at $5.
Fly.io~$2/mo upFree tier is gone for new accounts; pay per second of compute.

Prices and free tiers in this area change constantly — these were checked in September 2026. Verify before committing.

On a VPS, run it under systemd so it comes back after a crash or reboot:

[Unit]
Description=Discord bot
After=network-online.target

[Service]
User=bot
WorkingDirectory=/home/bot/app
EnvironmentFile=/home/bot/app/.env
ExecStart=/usr/bin/python3 bot.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Restart=always is the whole point. Networks drop, APIs rate-limit, processes die. Something must bring it back without you.

Request-driven bots

Serverless functions genuinely fit here: you pay per invocation, and a webhook that fires forty times a day costs nothing on any provider's free allowance.

Netlify Functions, Cloudflare Workers, Vercel Functions, AWS Lambda — all fine. Workers are the fastest to cold-start and the cheapest at low volume; Lambda is the most flexible if you need unusual runtimes or long timeouts.

Two constraints to design around: execution time limits (often tens of seconds — a slow scrape will be killed mid-run) and no persistent local state (the filesystem vanishes; use a real datastore).

For a chat widget backend, a function plus a hosted database is usually the cheapest thing that works, and scales to zero when nobody's talking to it.

Heavy and physical bots

Browser automation is the resource hog. Each Chromium instance wants a few hundred megabytes of RAM and a real CPU share. A $5 VPS with 1GB will thrash; budget 2GB minimum, and reuse one browser across many pages rather than launching per page. For occasional runs, GitHub Actions works — playwright install chromium in the workflow — but each run re-downloads the browser unless you cache it, which eats your minutes.

Anything with a sensor runs where the sensor is. That's a Pi or an ESP32 on your shelf, and the hosting question becomes a power and Wi-Fi question. Use systemd with Restart=always, and give it a way to tell you it's alive — a bot that silently stopped three weeks ago is the most common failure in this whole category.

The decision, as a table

Your botRun it onCost
Daily digest, no state keptGitHub ActionsFree
Hourly scraper with a databasePi, or a VPSFree – $5/mo
Discord / Slack listenerVPS or Pi, under systemdFree – $5/mo
Webhook handlerCloudflare Workers / Netlify FunctionsFree
Chat widget backendServerless + hosted databaseFree – $10/mo
Browser automation, occasionalGitHub Actions with a cached browserFree
Browser automation, constantVPS with 2GB+$7–15/mo
Sensor or cameraThe Pi it's attached to~$2/yr

The four things that break in production

Silent death. The worst failure mode in this field, because absence looks exactly like a quiet week. Fix it by making success visible: send "nothing needed you today" rather than nothing, or have the bot ping a dead-man's-switch service that alerts you when the ping stops.

Secrets in the repository. Use the platform's secret store — Actions secrets, an EnvironmentFile, your host's environment variables. Never commit the key, and rotate it immediately if you ever do.

No logs. Redirect output somewhere durable. When something breaks at 3am, the log is the only thing that tells you what happened.

Assuming state survives. Ephemeral platforms wipe the filesystem between runs. Decide deliberately where your bot's memory lives, and test it by running twice.

Start here

If you're not sure: GitHub Actions if it's scheduled and stateless, a Raspberry Pi or a cheap VPS for everything else. Those two cover the large majority of bots anyone actually builds, and between them cost between nothing and five dollars a month.

Once it's running, the bot-versus-agent piece covers keeping the running cost at zero, and the project library has builds that each state what they cost to keep alive.

One new build sheet a week

Guides like this one, plus a new build sheet every Thursday. Written by a person who built it.