VPS for trading bots: what actually matters
What a trading bot really needs from a VPS: restart discipline under systemd, a correct clock, safe API keys, heartbeats, and honest latency limits.
What a trading bot needs from a VPS
A VPS for trading bots is judged on four things: does the process come back after it dies, is the clock right, are the API (application programming interface) keys hard to steal, and do you find out when it stops. Raw speed is far down that list for a retail bot, because the slow part of your order path is your broker and the distance to it, not the host running your Python.
This is an engineering guide. Nothing here is financial advice, and no strategy is discussed.
Uptime is restart discipline, not a number on a sales page
Advertised uptime commitments differ. AWS specifies at least 99.5 percent for a single EC2 instance and 99.99 percent for qualifying deployments across multiple availability zones. Those commitments cover infrastructure availability, not your bot's health. A bot dies from an unhandled exception, a websocket that never reconnects, or the OOM (out of memory) killer, and the server stays up the whole time. So the useful question is what happens in the ten seconds after your process exits.
Run the bot as a systemd service and let the init system own the restart. A unit file does this.
[Unit]
Description=Trading bot
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=bot
WorkingDirectory=/opt/tradingbot
EnvironmentFile=/etc/tradingbot/api.env
ExecStart=/opt/tradingbot/venv/bin/python -m tradingbot
Restart=always
RestartSec=10
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/tradingbot
[Install]
WantedBy=multi-user.targetStartLimitIntervalSec=0 is the line people miss. Upstream systemd defaults allow 5 starts in 10 seconds, including the initial start and manual starts; the manager configuration can override those defaults. Hitting the limit stops automatic restart attempts, but the unit can be started manually or by a timer or socket after the interval passes. Setting it to 0 disables the rate limit, so a bot that crash-loops keeps trying instead of going quiet. RestartSec=10 stops that loop from hammering the exchange with reconnects.
Check the file before you trust it, then start it:
sudo systemd-analyze verify /etc/systemd/system/tradingbot.service
sudo systemctl daemon-reload
sudo systemctl enable --now tradingbot
systemctl status tradingbotenable is the half that survives a reboot, and kernel updates mean reboots. To see whether the bot has been quietly dying, ask systemd for the restart counter:
systemctl show tradingbot -p NRestarts
journalctl -u tradingbot --since "24 hours ago" | tail -50NRestarts=0 counts no automatic restart jobs since the counter was last reset; it does not prove a healthy or crash-free week. Manual restarts and systemctl reset-failed reset the counter, and a hung bot can still show zero. NRestarts=812 counts 812 restart jobs, not successful exchange reconnections or trades, and says nothing about when they occurred. Check the journal for the timing and cause, including failures before the bot connects. The full unit file anatomy, including timers for scheduled jobs like a daily report, is covered in running a program as a systemd service.
Set the clock to UTC and prove it is synchronised
Exchange APIs use timestamps to validate signed requests; Binance Spot defaults to a 5-second receive window and rejects timestamps at least 1 second ahead of server time. A drifting clock produces errors that look like authentication failures, so people rotate keys for hours before checking the time. On Binance-style APIs the message is literal: Timestamp for this request was 1000ms ahead of the server's time.
Set the server to UTC. Local time zones introduce daylight saving jumps in displayed times; Unix timestamps used by APIs do not jump when the display timezone changes.
sudo timedatectl set-timezone UTC
timedatectlUbuntu defaults to chrony from 25.10 onward; older installations can use systemd-timesyncd, an SNTP (simple network time protocol) client. systemd-timesyncd adjusts the clock, but it cannot poll multiple servers at once to detect an incorrect time source. Use chrony instead:
sudo apt update && sudo apt install -y chrony
sudo systemctl enable --now chrony
chronyc tracking
chronyc sources -vThe line to read from chronyc tracking is System time, for example System time : 0.000031415 seconds fast of NTP time. A small value shows a small remaining correction to chrony's NTP clock; check synchronisation, Root delay and Root dispersion too before judging accuracy. If it reads Leap status : Not synchronised, chrony is not synchronised, even if servers have responded; measurements can be rejected as too inaccurate. Wait a minute, then check again before touching firewall rules.
Keep API keys out of the places you copy
A leaked exchange key is worse than a leaked SSH key, because withdrawal permission turns it into money instantly. Two habits cover most of the risk.
First, never grant withdrawal permission to a bot key, and where the exchange supports it, bind the key to your server's IP address. This is the single control that makes a stolen key close to useless.
Second, keep the secret out of the code directory. Anything inside /opt/tradingbot ends up in a git repository or a backup archive sooner or later. Put it in a root-owned file that systemd reads:
sudo install -d -m 750 -o root -g bot /etc/tradingbot
sudo install -m 640 -o root -g bot /dev/null /etc/tradingbot/api.env
sudo nano /etc/tradingbot/api.envThe file holds plain KEY=value lines with no quotes and no export. Mode 640 gives the root owner read/write access and members of group bot read access, with no permissions for other users. Verify with sudo -u bot cat /etc/tradingbot/api.env and then with an unprivileged user outside group bot, where it must fail with Permission denied.
The bot itself should not run as root or as your login user. Create a system account with a nologin shell and /var/lib/tradingbot as its home directory before starting the service or creating files owned by group bot:
sudo useradd --system --shell /usr/sbin/nologin --home-dir /var/lib/tradingbot --create-home botThe reasoning behind each of those flags, and how far ProtectSystem=strict actually goes, is in running services as an unprivileged user. The rest of the server baseline, SSH keys and a firewall, belongs in the first ten minutes on a new VPS.
Find out it is down before your broker does
systemctl status says the process is running. It does not say the bot is doing anything. A process stuck in a retry loop against a dead websocket passes the process checks in the unit above.
Use a heartbeat instead. Uptime Kuma has push monitors: it expects your bot to call a URL on a schedule, and alerts when the call stops arriving. Put the call at the end of your main loop, after the part that proves the bot is alive, such as a successful market data read.
curl -fsS "http://monitor.example.com:3001/api/push/YOUR_TOKEN?status=up&msg=loop_ok"Set the monitor interval to roughly twice your loop time so normal jitter does not page you. Run the monitor on a different server from the bot, because a monitor that dies with the thing it watches reports nothing. Setup is covered in self-hosted status monitoring with Uptime Kuma.
Add a disk alert too. A bot writing verbose logs will fill the root filesystem in weeks, and a full disk stops the database write, not the network call, so the symptoms are strange. journalctl --vacuum-time=14d and a SystemMaxUse= line in /etc/systemd/journald.conf keep the journal bounded.
The honest part: latency is mostly not your host
This is where the market for trading VPS products stops being technical. Marketing pages quote sub-millisecond figures and imply the host is what stands between you and a fill. For almost every retail bot, it is not.
Your order travels from the bot to the exchange or broker endpoint over the public internet. That path is dominated by physical distance and by the peering between your provider and theirs. Then the broker's own systems add their queue, their risk checks and their rate limits.
Measure it rather than guessing. curl reports the connection and first-byte times for a real endpoint:
curl -s -o /dev/null -w 'dns=%{time_namelookup}s connect=%{time_connect}s ttfb=%{time_starttransfer}s\n' \
https://api.kraken.com/0/public/Time
mtr -rwzc 100 api.kraken.comRun that from a candidate server before you commit. If connect is 0.180 seconds, that includes DNS resolution and the TCP connection; it does not establish which continent you are on. If connect is 0.004 seconds and ttfb is 0.140 seconds, the difference includes TLS setup, request transmission, network delays and server processing. A host change can affect some of those components, so compare measurements from candidate locations.
So when does the host matter? When you are colocated or cross-connected to the venue and competing on queue position, which is a different business with a different budget. And when your own code is the bottleneck: a bot that recomputes indicators over a full history every tick can burn 200 milliseconds of CPU per loop, which is real latency you control for free. Profile the loop before you shop for a faster server.
What does matter in the host you pick is geography, stable networking, and enough memory that the OOM killer never gets a vote. Add memory if you keep tick history in a local database.
A short pre-live checklist
systemctl is-enabled tradingbotprintsenabled, and the service survivessudo reboot.chronyc trackingreports synchronisation, a system time offset under a few milliseconds, and root delay and dispersion within your timing budget.- The API key has trading permission, no withdrawal permission, and an IP allowlist if the exchange offers one.
- Killing the process with
sudo systemctl kill -s SIGKILL tradingbotbrings it back after theRestartSecdelay and startup time. - The heartbeat monitor pages you after the configured interval and retries when you stop the bot deliberately.
- Logs are bounded and the root filesystem has headroom in
df -h.
Run the whole thing in the exchange's sandbox or in paper mode for a week before real funds. Every item above fails at least once in that week, which is the point of the week.
FAQ
Does a trading bot need a low-latency or bare metal server?
Only if you are competing on execution speed against other automated participants at the same venue, which normally means colocation rather than a general purpose VPS. For a retail bot the round trip is dominated by geography and by the broker's own processing, so pick a server close to the API endpoint and measure with curl and mtr before paying for anything faster.
How much RAM and CPU does a trading bot need?
Most single-strategy bots are network-bound and idle between events. Memory becomes the constraint when you hold tick history in process or run a local database, so watch free -h and the journal for OOM kill messages rather than guessing.
Why does my exchange API reject requests with a timestamp error?
The request timestamp is outside the exchange's accepted window; clock drift and request delays both matter. Install chrony, confirm chronyc tracking shows a small System time offset and a synchronised leap status, and check root delay and dispersion too. Setting the machine to UTC keeps displayed times consistent but does not correct clock drift. Rotating the API key does not fix a clock problem.
How do I stop my bot from dying overnight without me knowing?
Run it under systemd with Restart=always and StartLimitIntervalSec=0 so a crash loop keeps retrying instead of hitting the start rate limit, then add a heartbeat that the bot sends at the end of each successful loop. The restart handles the process. The heartbeat catches the case where the process is alive but stuck.
Can I run the bot and my monitoring on the same VPS?
You can, and the monitoring will lie to you the day it matters, because an outage that takes down the bot takes the monitor with it. Keep the alerting on a separate machine, ideally with a different provider or region, and use the bot's server only for the bot and its logs.