Guides 16 min read ·
Run a Python Trading Bot 24/7 on Linux: systemd, Timers, Alerts
Keep a Python trading bot running on Ubuntu 24.04: systemd restarts, timers in IST, NSE holiday skips, daily re-login, Telegram alerts and a kill switch.
To run a Python trading bot 24/7 on Linux, run it as a systemd service that restarts on failure, start and stop it with systemd timers set to IST, and send yourself an alert when it fails. tmux, nohup and cron can't do all three.
This guide sets that up with the algo@ template that comes ready on our Algo VPS. It works on any Ubuntu 24.04 server (systemd 255).
Why systemd, not nohup, screen, tmux or cron
A bot has to survive its own crashes, server reboots and your SSH session closing:
| Method | Restarts after a crash | Starts after a reboot | Logs |
|---|---|---|---|
nohup python bot.py & | No | No | A nohup.out file that keeps growing |
| tmux or screen | No | No | Only the scrollback |
| cron | Only by polling, at most once a minute | With @reboot | Whatever you redirect |
| systemd | Yes, with limits you set | Yes, once enabled | The journal, timestamped and size-capped |
systemd also gives you calendar timers with time zones and a hook that runs when the bot fails.
Use tmux, which is installed on the Algo VPS, for watching the bot: follow its logs in one pane, detach with Ctrl-b d and come back later with tmux attach.
Run the bot with the algo@ template, or write your own unit
On the Algo VPS the template lives at /etc/systemd/system/algo@.service. Without its comments, it reads:
[Unit]
Description=Algo bot %i
After=network-online.target time-sync.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/algo
EnvironmentFile=-/opt/algo/.env
ExecStart=/opt/algo/venv/bin/python /opt/algo/%i.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
%i is the part after the @, so a bot saved as /opt/algo/mybot.py runs as algo@mybot:
sudo systemctl enable --now algo@mybot # start now and after every reboot
journalctl -u algo@mybot -f # follow the logs
Note that the template sets no User=, so bots run as root. Every bot also reads the same /opt/algo/.env, so each bot can see every secret in it.
Write your own unit if you want a non-root user (the static IP guide has one with User=), separate secrets per bot, or to run anything that isn't a single script, such as a TradingView webhook receiver or OpenAlgo. The rest of this guide still applies.
A plain Cloud VPS works just as well if you're happy to set it up yourself. Create the virtual environment, save the unit above as /etc/systemd/system/algo@.service and run sudo systemctl daemon-reload. Cloud VPS plans start at ₹860 a month, or about ₹774.17 a month when billed yearly, and Mumbai adds a monthly location fee. Algo VPS Mumbai comes with all of this ready, for ₹1,510 a month, or about ₹1,325 a month when billed yearly.
Stop crash loops before they hammer your broker
Restart=on-failure with RestartSec=10 brings the bot back 10 seconds after a crash. Ubuntu 24.04's default rate limit is 5 starts in 10 seconds, and a 10-second delay never reaches it. Say the bot dies at start-up from an expired token. It then restarts up to six times a minute, all day, and may log in to your broker every time.
Set your own limit. Run sudo systemctl edit algo@mybot and add:
[Unit]
StartLimitIntervalSec=15min
StartLimitBurst=5
OnFailure=algo-alert@%i.service
If the bot then needs more than five starts in 15 minutes, systemd stops restarting it and marks it failed. It also starts algo-alert@mybot, the Telegram alert built below. The next morning's timer can still start the bot. To retry sooner, fix the cause and run sudo systemctl reset-failed algo@mybot.
Make restarts safe too: on start-up, read open orders and positions from the broker first, so a restarted bot doesn't repeat an entry.
Start before the open and stop after the close with systemd timers
NSE's equity market takes pre-open orders from 09:00 to 09:08 and runs its normal market from 09:15 to 15:30 IST (NSE circular). Starting at 08:45 leaves time to log in and load instruments. Save /etc/systemd/system/algo@.timer:
[Unit]
Description=Start algo bot %i before the market opens
[Timer]
OnCalendar=Mon..Fri 08:45 Asia/Kolkata
AccuracySec=1s
[Install]
WantedBy=timers.target
In the systemd.time syntax, Mon..Fri is a range of weekdays. The time zone at the end makes the timer fire at 08:45 IST even on a server set to UTC.
AccuracySec defaults to one minute, so set it lower. A timer starts the service with the same name, so algo@mybot.timer starts algo@mybot.service. To check the expression:
systemd-analyze calendar --iterations=3 "Mon..Fri 08:45 Asia/Kolkata"
A timer can't stop a service. Stopping needs a one-shot service, /etc/systemd/system/algo-stop@.service, and a second timer, algo-stop@.timer:
[Unit]
Description=Stop algo bot %i after the close
[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl stop algo@%i.service
[Unit]
Description=Stop algo bot %i after the close
[Timer]
OnCalendar=Mon..Fri 15:45 Asia/Kolkata
AccuracySec=1s
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now algo@mybot.timer algo-stop@mybot.timer
systemctl list-timers 'algo*'
Keep algo@mybot itself enabled, so a reboot at 11:00 brings the bot back. The holiday check below stops it starting at night.
Treat the stop timer as a backstop. systemctl stop sends SIGTERM, then SIGKILL after 90 seconds, and by default Python dies on SIGTERM without cleaning up. It's better for the bot to end its own day with exit status 0, which Restart=on-failure doesn't restart. The skeleton below does this.
Check the clock first
Timers, log timestamps and the TOTP codes some broker logins use all depend on the server's clock. The Algo VPS runs on Asia/Kolkata time and chrony keeps the clock in sync. To check:
timedatectl # look for "System clock synchronized: yes"
chronyc tracking # "Leap status" should be Normal; "System time" is the offset
If the leap status reads "Not synchronised", the clock isn't being synced (see the chronyc manual). On a plain server, run sudo timedatectl set-timezone Asia/Kolkata and sudo apt install chrony.
Skip NSE trading holidays, and decide about special sessions
NSE circular NSE/CMTR/71775 (12 December 2025) lists 15 weekday trading holidays in the capital market segment for 2026. F&O has the same dates, but the debt segment has 19, so use your segment's list. NSE's holidays page has every segment.
Holidays can also be added during the year. On 12 January 2026, NSE made 15 January a holiday for Maharashtra's municipal elections, at three days' notice. Keep the dates in a file and update it whenever a new circular appears. As of 19 October, the capital market holidays left in 2026 are:
# /opt/algo/holidays.txt (NSE capital market, circular NSE/CMTR/71775)
2026-10-20 # Dussehra
2026-11-10 # Diwali Balipratipada
2026-11-24 # Prakash Gurpurb Sri Guru Nanak Dev
2026-12-25 # Christmas
This helper exits with status 0 on a trading day inside a given time window:
# /opt/algo/trading_day.py: exit 0 on an NSE trading day inside the window, else 1
# Usage: trading_day.py 08:30 15:35
import sys
from datetime import datetime, time
from pathlib import Path
from zoneinfo import ZoneInfo
now = datetime.now(ZoneInfo("Asia/Kolkata"))
try:
lines = Path("/opt/algo/holidays.txt").read_text().splitlines()
except OSError:
sys.exit(255) # no list: fail loudly
holidays = {line.split("#")[0].strip() for line in lines}
if not any(h.startswith(str(now.year)) for h in holidays):
sys.exit(255) # list not updated for this year
start, end = (time.fromisoformat(t) for t in sys.argv[1:3])
trading = now.weekday() < 5 and now.date().isoformat() not in holidays
sys.exit(0 if trading and start <= now.time() <= end else 1)
Hook it in with ExecCondition=. Run sudo systemctl edit algo@mybot again and add:
[Service]
ExecCondition=/opt/algo/venv/bin/python /opt/algo/trading_day.py 08:30 15:35
According to the systemd.service manual, exit codes 1 to 254 skip the start quietly, while 255 marks the unit failed. So the bot simply doesn't start on a holiday, at night or after a late crash. A missing or out-of-date holiday list fails the unit, which triggers your alert.
Special sessions. NSE will hold Muhurat trading on Sunday 8 November 2026. On 19 October its holidays page still said the timings would be "notified subsequently through a circular". The helper treats weekends as closed, so the bot won't start. Trading the session would also mean changing your bot's own session times, so treat it as a separate, tested run.
Plan for the daily re-login
NSE's implementation standards say: "All API sessions shall be compulsorily logged out every day before the start of the next trading day." Zerodha's Kite Connect docs, for example, say the access token expires "at 6 AM on the next day (regulatory requirement)". So every morning:
- Get a fresh session first. Either the bot logs in itself, where your broker allows that, or it picks up the token from your manual morning login.
- Save the day's token with its date in a
chmod 600file, so a restart that day reuses it. - Test the token with a cheap read call, such as profile or funds, before you trade.
- If the token is missing or rejected, send an alert and wait. Poll for a new token. Don't exit and let systemd hammer the login endpoint.
The login step differs by broker. Kite needs a fresh login on its web login page, which redirects to your app with a request token. Dhan has its own token options. The Kite Connect guide and the Dhan API guide cover both, including what each broker allows on automating the login.
Health checks, a heartbeat and Telegram alerts
systemd and OnFailure= deal with crashes. A bot that hangs while its process stays alive needs a heartbeat file. A server that goes down can't report anything itself, so it needs a monitor outside it.
Telegram alerts
To create a bot, message @BotFather on Telegram and send /newbot. Keep the token it gives you secret, as Telegram advises. Send your new bot a message first, because a bot can't message you until you do. Then open https://api.telegram.org/bot<token>/getUpdates and note the chat → id value. Add both values to /opt/algo/.env:
TG_TOKEN=replace-with-your-bot-token
TG_CHAT=replace-with-your-chat-id
The sendMessage method needs only chat_id and text, and the text can be up to 4,096 characters:
# /opt/algo/alert.py
import logging
import os
import sys
import requests
def send(text):
"""Send a Telegram message. Never raises, never blocks for long."""
try:
requests.post(
f"https://api.telegram.org/bot{os.environ['TG_TOKEN']}/sendMessage",
data={"chat_id": os.environ["TG_CHAT"], "text": text[:4000]},
timeout=5,
)
except Exception as exc:
# log only the type: the error text can contain the URL, and the URL contains the token
logging.warning("Telegram alert failed: %s", type(exc).__name__)
if __name__ == "__main__":
send(" ".join(sys.argv[1:]))
Next, save the unit that OnFailure= starts as /etc/systemd/system/algo-alert@.service:
[Unit]
Description=Telegram alert for algo bot %i
[Service]
Type=oneshot
EnvironmentFile=/opt/algo/.env
ExecStart=/opt/algo/venv/bin/python /opt/algo/alert.py "algo@%i has failed on %H. Check journalctl -u algo@%i"
Run sudo systemctl daemon-reload, then test the alert with sudo systemctl start algo-alert@test. Your bot can also import send to report fills and errors. Telegram's FAQ asks for no more than about one message per second per chat.
A heartbeat in the main loop
Touch a heartbeat file after each pass of the loop that actually worked, such as one that got fresh ticks. Don't touch it from a separate thread, which would keep the file fresh while the loop is stuck. The skeleton also handles the kill switch and SIGTERM:
# /opt/algo/mybot.py: skeleton only; your broker calls go where the comments are
import logging
import signal
import sys
import time
from datetime import datetime, time as dtime
from pathlib import Path
from zoneinfo import ZoneInfo
from alert import send
IST = ZoneInfo("Asia/Kolkata")
HEARTBEAT = Path("/opt/algo/mybot.heartbeat")
KILL = Path("/opt/algo/KILL")
END_OF_DAY = dtime(15, 30)
running = True
def on_sigterm(signum, frame):
global running
running = False
def main():
# log in or load today's token, then read open orders and positions from the broker
send("mybot started")
while running and datetime.now(IST).time() < END_OF_DAY:
if KILL.exists():
# cancel every open order through your broker's SDK here
send("Kill switch: open orders cancelled, mybot stopped")
return
# one pass of your strategy loop here
HEARTBEAT.touch()
time.sleep(5)
send("mybot finished for the day")
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
signal.signal(signal.SIGTERM, on_sigterm)
try:
main()
except Exception:
logging.exception("mybot crashed")
send("mybot crashed; systemd will restart it")
sys.exit(1)
An outside dead man's switch
A dead man's switch is an outside service that expects a regular ping and alerts you when the pings stop. Healthchecks.io's free Hobbyist plan monitors up to 20 jobs as of October 2026, and Telegram is one of its alert channels. Create a check with a one-minute period and a few minutes' grace, then add HC_URL=https://hc-ping.com/your-check-uuid to .env.
The script below pings that URL every minute. If the bot should be trading but its heartbeat is more than two minutes old, the script adds /fail to the URL. Healthchecks.io's API docs say this signals a failure straight away.
#!/bin/sh
# /opt/algo/check.sh <bot>: run every minute (chmod +x)
HB="/opt/algo/$1.heartbeat"
if /opt/algo/venv/bin/python /opt/algo/trading_day.py 09:20 15:25; then
age=$(( $(date +%s) - $(stat -c %Y "$HB" 2>/dev/null || echo 0) ))
if [ "$age" -gt 120 ]; then
curl -fsS -m 10 --retry 3 -o /dev/null "$HC_URL/fail"
exit 0
fi
fi
curl -fsS -m 10 --retry 3 -o /dev/null "$HC_URL"
To run it every minute, save /etc/systemd/system/algo-check@.service and algo-check@.timer:
[Unit]
Description=Health check for algo bot %i
[Service]
Type=oneshot
EnvironmentFile=/opt/algo/.env
ExecStart=/opt/algo/check.sh %i
[Unit]
Description=Run the health check for algo bot %i
[Timer]
OnCalendar=minutely
[Install]
WantedBy=timers.target
Run sudo systemctl daemon-reload, then sudo systemctl enable --now algo-check@mybot.timer. The check runs around the clock, so Healthchecks.io notices a dead server at night too.
Logs: journald retention and what to log
Anything the bot writes to stdout or stderr goes to the journal, with a timestamp:
journalctl -u algo@mybot --since today
journalctl -u algo@mybot --since "2026-10-16 09:00" --until "2026-10-16 15:30"
journalctl --disk-usage
On Ubuntu 24.04, journald caps the journal at 10% of the file system by default, up to 4 GB (see journald.conf). It keeps as much history as fits, not a fixed number of days. To set a size and a maximum age yourself:
sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/retention.conf <<'EOF'
[Journal]
SystemMaxUse=2G
MaxRetentionSec=90d
EOF
sudo systemctl restart systemd-journald
Good logging habits:
- Use
logging, or setPYTHONUNBUFFERED=1in.env. Python buffers stdout in blocks when it isn't a terminal, soprint()lines can reach the journal minutes late. stderr, whereloggingwrites by default, is line-buffered. - Write one line per event, with the broker's order ID and the symbol.
- Never log secrets. Keep API keys, tokens, TOTP secrets and full request URLs out of the logs.
- Keep your own trade record in a CSV file or an SQLite database. The journal rotates.
A kill switch you can pull from your phone
Stopping the service doesn't touch anything already at the broker: open orders stay open and positions stay on. A kill switch should cancel open orders and refuse new ones. The skeleton above checks for a file called /opt/algo/KILL, which you can create from an SSH app on your phone:
sudo touch /opt/algo/KILL # bot cancels open orders, alerts you and exits
sudo rm /opt/algo/KILL # only when you're ready to trade again
The bot exits with status 0, so systemd doesn't restart it. While the file exists, the bot also stops at its first check when the timer starts it the next morning. Make your order function check the file as well.
If you add a /kill command to your Telegram bot, accept it only from your own chat ID, and never add commands that place orders.
Brokers' own kill switches work even when your server can't be reached. As of October 2026:
- Zerodha's kill switch disables a segment from account.zerodha.com or the Kite app, once open orders and positions are closed. You can re-enable it after 12 hours.
- Dhan's Kill Switch API disables trading for the rest of the day. It also needs positions closed and no pending orders.
Updates, reboots and backups
Keep updates and reboots outside market hours
Ubuntu 24.04 installs security updates through apt-daily-upgrade.timer. Its timer file schedules this for 06:00 server time, plus a random delay of up to 60 minutes. That's well before the open, with two catches:
- Missed runs catch up. If the server was off at 06:00, the update runs at the next boot, which could be mid-session. Ubuntu's automatic updates guide gives the fix: run
sudo systemctl edit apt-daily-upgrade.timerand addPersistent=falseunder[Timer]. - Services restart by themselves. unattended-upgrades calls needrestart, which since Ubuntu 24.04 restarts affected services automatically. To exclude your bots and restart them yourself after the close:
echo '$nrconf{override_rc}{qr(^algo)} = 0;' | sudo tee /etc/needrestart/conf.d/algo.conf
Automatic reboots are off by default (Unattended-Upgrade::Automatic-Reboot "false" in /etc/apt/apt.conf.d/50unattended-upgrades). If you turn them on, change Automatic-Reboot-Time from its default of "now" to a night-time hour such as "02:00". Or leave them off, and reboot by hand at the weekend whenever /var/run/reboot-required exists.
unattended-upgrades never touches your virtual environment. Pin your packages with /opt/algo/venv/bin/pip freeze > /opt/algo/requirements.txt, and upgrade broker SDKs on purpose at the weekend, after reading the changelog.
Back up code and config
- Code: keep it in a private git repository, so the server is never the only copy.
- Config is the part people forget. Bundle it and copy it to your own computer:
sudo tar czf /root/algo-backup-$(date +%F).tar.gz /etc/systemd/system/algo* \ /opt/algo/*.py /opt/algo/holidays.txt /opt/algo/requirements.txt scp root@your-server-ip:/root/algo-backup-*.tar.gz . # run this on your computer - Secrets: keep them out of git and the archive. Store the
.envvalues in a password manager, and regenerate your keys at the broker if the server is ever compromised. - Trade records: copy them off the server every day.
Test a rebuild once. On the Algo VPS the IPv4 address survives an operating-system reinstall, so you won't need to register a new one.
Frequently asked questions
Is tmux or nohup enough to keep a trading bot running 24/7?
No. tmux and nohup keep a process alive after you log out, but neither restarts the bot after a crash or starts it again after a reboot. A systemd service with Restart=on-failure does both and sends the bot's output to the journal. tmux is still handy for watching: follow journalctl -u algo@mybot -f in a tmux pane and detach while systemd keeps the bot running.
How do I start my trading bot only on NSE trading days?
Use a systemd timer with OnCalendar=Mon..Fri 08:45 Asia/Kolkata to start the bot on weekdays, and add a holiday check. Copy NSE's trading holidays for your segment into a file, and have a small script exit 0 only on a weekday that isn't listed. Run that script as ExecCondition= in the service so systemd quietly skips the start on holidays. Update the file whenever NSE issues a new holiday circular.
How can I get an alert on my phone when my trading bot stops?
Use two layers of alerts. On the server, add OnFailure= to the bot's service so that, when the bot fails, systemd starts a small unit that sends you a Telegram message through the Bot API's sendMessage method. Outside the server, use a dead man's switch such as Healthchecks.io: a script pings it every minute, and if the pings stop because the server or the bot is down, it alerts you.
Will Ubuntu's automatic updates restart my bot during market hours?
They can. On Ubuntu 24.04 the update timer runs at 06:00 plus up to an hour's random delay, before the market opens, but a missed run catches up at the next boot. needrestart also restarts affected services automatically after updates. Set Persistent=false on apt-daily-upgrade.timer, exclude your bot services in needrestart, and either keep automatic reboots off or set them to a night-time hour.