n8n on a VPS: automations with no execution cap

n8n's Community edition does not count executions: the cap is your machine. We measured it on a Nano with a real webhook, processing and HTTP request workflow, then covered backing up the encryption key, updates and what the license allows.

Three nodes chained together, a webhook, a processing step and an HTTP request, with a queue of calls arriving from the left

n8n connects your tools: a form that creates a customer record, an order that lands in the accounting system, an alert that shows up in a chat channel. The plans n8n hosts for you are priced by executions per month (2,500 on the smallest one, according to its pricing page in November 2026). The Community edition counts nothing: on your own server, the only cap is the machine.

We measured that cap in Montréal: our n8n image, a manual install with Docker Compose, a real workflow (webhook, processing, HTTP request) and thousands of calls. Then backups, updates and the license.

1. Which plan to pick

n8n runs as a single Node.js process. At rest, after an hour of work, its container used 373 MB of memory and Caddy about 20 MB. During our load tests, n8n never went above 1,023 MB. A Nano (1 vCPU, 2 GB) is enough to start with: we saw no crash.

The CPU runs out before the memory does (section 5). We did not test workflows that handle files (PDFs, images, large CSV exports): for those, plan for more memory. To try it out, take an hourly Nano (0.018 CAD an hour) and destroy it afterwards.

2. Deploy with our n8n image

The catalog has an n8n image, available from the Nano up. It is Ubuntu 24.04 with Docker, with n8n running behind Caddy. Every VM gets its own domain name (shaped like vm-xxxxxxxx.ffxf.zone): on first boot, Caddy gets a Let's Encrypt certificate for it, and n8n answers over HTTPS at https://your-vm-domain. In our test, the certificate was in place less than two minutes after the order. Everything lives in /opt/n8n: compose.yml, .env and Caddyfile. The SSH user is ubuntu.

Two things to do right away. First, create the owner account immediately: whoever fills in the welcome screen first becomes the owner of the instance. Certificates are published in public logs, and a scanning bot hit our VM one second after its certificate was issued.

Then update n8n. The image ships the version pulled when it was built: 2.35.3 in our test, while the stable release that day was 2.46.4. We have rebuilt it since, but the gap comes back with every new n8n release. The compose file follows the latest tag, so pulling the new image is enough:

bash
cd /opt/n8n
sudo docker compose pull
sudo docker compose up -d
sudo docker compose exec n8n n8n --version

To use your own domain, point its A and AAAA records at the machine, replace the VM name in the Caddyfile and in WEBHOOK_URL (in .env), then run sudo docker compose up -d again.

3. Install by hand with Docker Compose

A manual install lets you pick the version and set the encryption key yourself. We did it on a Nano with the catalog's Docker image (see Docker on a VPS to start from a bare Ubuntu). Replace DOMAIN with your VM's name or your own domain:

bash
sudo mkdir -p /opt/n8n && cd /opt/n8n
DOMAIN=vm-xxxxxxxx.ffxf.zone
printf 'DOMAIN=%s\nN8N_ENCRYPTION_KEY=%s\n' "$DOMAIN" "$(openssl rand -hex 32)" | sudo tee .env >/dev/null
sudo chmod 600 .env
printf '%s {\n\treverse_proxy n8n:5678\n}\n' "$DOMAIN" | sudo tee Caddyfile

Then compose.yml:

yaml
services:
  n8n:
    image: docker.n8n.io/n8nio/n8n:2.46.4
    restart: unless-stopped
    environment:
      - N8N_HOST=${DOMAIN}
      - N8N_PROTOCOL=https
      - N8N_PROXY_HOPS=1
      - WEBHOOK_URL=https://${DOMAIN}/
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - GENERIC_TIMEZONE=America/Toronto
      - TZ=America/Toronto
      - EXECUTIONS_DATA_MAX_AGE=168
      - EXECUTIONS_DATA_PRUNE_MAX_COUNT=20000
    volumes:
      - n8n_data:/home/node/.n8n
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
volumes:
  n8n_data:
  caddy_data:
bash
sudo docker compose up -d

Download and first start took 1 min 10 s on the Nano. A few settings are worth explaining:

  • WEBHOOK_URL: without it, n8n shows webhook addresses on http://localhost:5678, which nobody outside can reach.
  • N8N_PROXY_HOPS=1: tells n8n a reverse proxy (Caddy) sits in front of it, so it reads the real client address.
  • GENERIC_TIMEZONE: the time zone for scheduled triggers. Without it, a workflow set for "every day at 8 AM" runs on New York time, n8n's default.
  • EXECUTIONS_DATA_MAX_AGE and EXECUTIONS_DATA_PRUNE_MAX_COUNT: by default n8n keeps execution history for 14 days and 10,000 entries; here, 7 days and 20,000. With SQLite, freed space is reused but not returned to the disk.

Caddy gets the certificate on its own if ports 80 and 443 are open. With Nginx (see our Nginx and HTTPS guide), pass the Upgrade and Connection headers: the editor uses a WebSocket connection.

4. A test workflow: webhook, processing, HTTP request

To measure something realistic, we built a four-node workflow:

  • Webhook: receives a POST on /webhook/commande and replies with the last node's output;
  • Calcul (Code node): adds up the order lines and cleans the customer's address;
  • Envoi (HTTP Request node): sends the result as JSON to an HTTPS receiver on another VM;
  • Reponse (Code node): returns the computed data and confirms the delivery.

The Calcul node's code:

bash
const b = $input.first().json.body;
const lignes = b.lignes || [];
const total = lignes.reduce((s, l) => s + l.qte * l.prix, 0);
return [{ json: {
  commande: b.id,
  client: String(b.client || '').trim().toLowerCase(),
  nb_lignes: lignes.length,
  total: Math.round(total * 100) / 100,
  recu_le: new Date().toISOString(),
} }];

Once the workflow is published, one call checks it:

bash
curl -s -X POST https://vm-xxxxxxxx.ffxf.zone/webhook/commande \
  -H 'Content-Type: application/json' \
  -d '{"id":"C-1042","client":"  Marie@Exemple.ca ","lignes":[{"qte":2,"prix":12.5},{"qte":1,"prix":30}]}'
bash
{"commande":"C-1042","client":"marie@exemple.ca","nb_lignes":2,"total":55,"recu_le":"2026-11-03T15:32:04.199Z","transmis":true}

The full round trip, outbound HTTPS call included, had a 0.16 s median over 100 calls sent one at a time.

5. At rest and under 1,000 calls

Calls came from another VM in the same data centre, using hey; memory and CPU were sampled every second on the n8n machine (docker stats and vmstat).

bash
hey -n 1000 -c 10 -m POST -T application/json -D body.json \
  https://vm-xxxxxxxx.ffxf.zone/webhook/commande

All 3,100 calls got a 200 response, with no errors and no restarts. But throughput tops out around 5 executions per second: under load, the vCPU was over 80% busy, almost entirely with n8n's main process. Sending more calls at once does not go faster, it only makes the queue longer: at 50 concurrent, each call waited 9 to 12 seconds.

A workflow reduced to a webhook that replies right away tops out at 6.8 executions per second; webhook plus Code node without the HTTP request, at 8.5 (300 calls, 10 concurrent). Turning off saving of successful executions (EXECUTIONS_DATA_SAVE_ON_SUCCESS=none) made no measurable difference. Most of the cost is n8n itself, on every execution, whatever the workflow.

At 5 per second, a Nano can in theory handle 18,000 executions an hour, far more than most small-business automations need. It is a poor fit, though, for a public webhook that receives bursts: calls pile up and an impatient sender will eventually give up. We could not measure the Starter during this test. Since the main process does nearly all the work, do not count on a second vCPU doubling throughput until you have checked. The scaling path n8n documents is queue mode, included in the Community edition, with separate workers.

On disk, the data folder weighed 53 MB after about 3,000 saved executions, event logs included.

6. Backups and the encryption key

All of n8n's state lives in the volume mounted on /home/node/.n8n: the SQLite database (workflows, users, history) and the config file that holds the encryption key. n8n encrypts your credentials (API tokens, passwords) with that key before writing them to the database. A database without its key is useless for credentials.

Our image does not set N8N_ENCRYPTION_KEY: n8n generated the key on first start and stored it in config, inside the volume. With the manual install above, the key is also in /opt/n8n/.env. Keep a copy somewhere other than the server, a password manager for example.

n8n's documentation says to stop n8n before copying the folder when using SQLite. On the Nano, stopping, archiving and restarting took 5.7 s:

bash
cd /opt/n8n
sudo docker compose stop n8n
sudo docker run --rm -v n8n_n8n_data:/data:ro -v /opt/n8n:/backup alpine \
  tar czf /backup/n8n-$(date +%F).tar.gz -C /data .
sudo docker compose start n8n

Then copy the archive (3.3 MB in our case) and the .env file off the machine. We restored that archive on the other VM: the workflow was published, the webhook answered and the test credential could be read. To restore:

bash
sudo docker compose stop n8n
sudo docker run --rm -v n8n_n8n_data:/data -v /path/to/archive:/backup alpine \
  sh -c "rm -rf /data/* && tar xzf /backup/n8n-YYYY-MM-DD.tar.gz -C /data"
sudo docker compose start n8n

The trap: if N8N_ENCRYPTION_KEY is set and does not match the key in the restored config file, n8n refuses to start and restarts in a loop with this message:

bash
Error: Mismatching encryption keys. The encryption key in the settings file /home/node/.n8n/config does not match the N8N_ENCRYPTION_KEY env var. Please make sure both keys match.

The n8n export:workflow --backup and n8n export:credentials --backup exports are for moving workflows, but they leave out users, history and the key: they do not replace a copy of the volume. Machine backups in the console add a second copy (7 manual ones included, automatic ones at 3 CAD a month).

7. Updates

n8n ships a minor release almost every week. Its documentation recommends updating at least once a month, reading the release notes and taking a full backup first. With the version pinned in compose.yml:

bash
cd /opt/n8n
sudo sed -i 's|n8nio/n8n:2.46.4|n8nio/n8n:NEW_VERSION|' compose.yml
sudo docker compose pull
sudo docker compose up -d

Our move from 2.35.3 to 2.46.4 ran the database migrations on its own. The container answered /healthz 13 seconds after the restart, but webhooks returned 404 for up to 25 more seconds, while n8n reactivated the published workflows. Plan for that gap, or update at a quiet hour.

On November 3, 2026, stable pointed to 2.46.4, but a 3.1.0 was already published. Before a major version jump, try it on a copy: an hourly VM restored from your archive.

8. License and limits

n8n is not open source by the Open Source Initiative's definition. It is published under the Sustainable Use License, which n8n files under the "fair-code" model: source available, free to use, with commercial use restricted. According to the license FAQ:

  • allowed: automating your own business, personal projects, building workflows for clients on your instance as long as they cannot create or edit them, charging for building, setting up and maintaining workflows;
  • not allowed without a commercial agreement: hosting n8n as a service where your clients build their own workflows, letting them configure the logic through your UI, the API or an AI agent, reselling n8n under your own brand.

The Community edition includes almost everything. According to the documentation, it lacks: projects and sharing workflows and credentials between users, single sign-on (SAML, LDAP), environments and Git version control, external secrets, custom variables, external storage for binary data, log streaming and multi-main mode. A free registration by email unlocks folders, debugging in the editor and custom execution data.

Finally, SQLite is fine for a single instance; queue mode, with several workers, needs PostgreSQL and Redis. Running the server is your job (see our guide to securing Ubuntu). The other images in the catalog are on the Images page.

Photo thumbnails rising from a phone to a server, where they line up in a grid

Immich: replace Google Photos on your own server

Phone backup, faces, search by what is in the picture: Immich covers most of Google Photos, on a machine you administer in Canada. Which plan to take, our image or a manual install, 3-2-1 backups and the limits.

Read the article 7 min read

An app panel in a window, one tile being deployed

Easypanel VPS: preinstalled, ready in two minutes

Easypanel joins the catalogue: a VPS delivered with the Docker panel already installed, answering on port 3000 less than two minutes after the order.

Read the article 4 min read

Support & discussions

Technical questions, incident reports, or infrastructure discussions, the team is reachable on Discord, Telegram, X, Instagram, Reddit, and IRC.