One request and you have a time series.
No signup and no setup step. Your first write picks your account name and key.
curl https://plotpoint.torvald.no/m/your-account/pick-a-key/g/cpu_temp/41.5
Then open https://plotpoint.torvald.no/d/your-account/pick-a-key to see your chart.
Live: writes to this service
counter · writes per minute, all accounts, last hourThis is a counter: how many write requests the service received each minute, from everyone. It is not the
cpu_temp value from the example above. Counter charts always show at least 0–5 on the y-axis, so a quiet minute with one or two writes doesn't fill the chart.
How keys work
- An account is any name you pick. A key is a password you pick, at least 8 URL-safe characters (
A-Z a-z 0-9 . _ ~ -). - The first write to a name claims it and stores that key permanently. After that, every write with a different key gets
403. - The free tier has no recovery. If you lose the key, the account is gone. Pick a new name. Paid accounts get support, including key resets.
- Write URLs end up in shell history, cron files and proxy logs. Use a throwaway key, never a password you use anywhere else.
- The dashboard URL contains the key, so treat it like the key. To show someone your charts, give them a read-only
/p/…link from the dashboard.
Endpoints
| Request | What it does |
|---|---|
GET|POST /m/{acct}/{key}/g/{metric}/{value} | Gauge: records a value at a point in time |
GET|POST /m/{acct}/{key}/c/{metric}[/{n}] | Counter: adds n, default 1. Charted as the total per time step. |
GET|POST /m/{acct}/{key}/t/{metric}/{ms} | Timing, in milliseconds |
GET|POST /m/{acct}/{key}/e/{name}/{text} | Event: URL-encoded text, drawn as a marker on every chart |
?host=nas&room=attic | Query parameters become labels (max 8) |
?ts=1767225600 | Sets the timestamp (unix seconds or milliseconds) |
GET /d/{acct}/{key} | Your dashboard |
GET /q/{acct}/{key}/{metric}?from=-24h&to=now | Series as JSON: {"t":[…],"v":[…],"events":[…]}. Optional &step=60 (10, 15, 30, 60, 120, 300 … seconds) sets the resolution. |
GET /s/{acct}/{key} | Your metrics, with last value and when each was last seen |
GET /p/{token} | Read-only shared view (create one from the dashboard) |
GET /badge/{token}/{metric}.svg | Status badge |
Responses: 204 stored (empty body) · 400 invalid input · 403 wrong key · 429 over a limit, with the reason in a JSON body.
Names (account, metric, label key) must match ^[a-zA-Z_][a-zA-Z0-9_.:-]{0,63}$. The label name acct is reserved.
Examples
Replace your-account/pick-a-key with your own. Every snippet has a copy button.
Shell basics
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
curl -s "$U/g/disk_free_gb/$(df -BG --output=avail / | tail -1 | tr -dc 0-9)?host=$(hostname)"
curl -s "$U/c/backups_ok"
curl -s "$U/e/deploy/$(printf 'v1.4 shipped' | jq -sRr @uri)"
POST works too
curl -s -X POST "https://plotpoint.torvald.no/m/your-account/pick-a-key/c/signups/3"
Linux host health
Drop this in /usr/local/bin/pp-health and run it from cron every minute. It sends six gauges per host.
#!/bin/sh
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
H="host=$(hostname)"
read l1 l5 l15 _ < /proc/loadavg
mem=$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo)
disk=$(df --output=pcent / | tail -1 | tr -dc 0-9)
up=$(cut -d' ' -f1 /proc/uptime)
temp=$(( $(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0) / 1000 ))
for kv in load1=$l1 load5=$l5 mem_avail_mb=$mem disk_used_pct=$disk uptime_s=$up cpu_temp=$temp; do
curl -s -o /dev/null "$U/g/${kv%%=*}/${kv#*=}?$H"
done
* * * * * /usr/local/bin/pp-health
Failed systemd units
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/failed_units/$(systemctl --failed --no-legend | wc -l)?host=$(hostname)"
Pending apt upgrades (Debian/Ubuntu)
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/apt_upgradable/$(apt list --upgradable 2>/dev/null | grep -c upgradable)?host=$(hostname)"
Cron jobs and backups
Count successes and failures separately, and record how long the job took.
#!/bin/sh
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
t0=$(date +%s%3N)
if restic backup /home; then
curl -s "$U/c/backup_ok?job=home"
else
curl -s "$U/c/backup_failed?job=home"
fi
curl -s "$U/t/backup_ms/$(( $(date +%s%3N) - t0 ))?job=home"
Every 5 minutes: 1-minute load average
*/5 * * * * curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/load1/$(cut -d' ' -f1 /proc/loadavg)?host=$(hostname)" >/dev/null
GitHub Actions
Put the write URL in a repository secret named PLOTPOINT_URL (for example https://plotpoint.torvald.no/m/your-account/pick-a-key) so the key stays out of logs.
- run: echo "T0=$(date +%s)" >> "$GITHUB_ENV" # first step of the job
# … your build and test steps …
- name: Report to Plotpoint
if: always()
env:
U: ${{ secrets.PLOTPOINT_URL }}
run: |
curl -s "$U/c/ci_${{ job.status }}?repo=${{ github.event.repository.name }}"
curl -s "$U/t/ci_ms/$(( ($(date +%s) - T0) * 1000 ))?repo=${{ github.event.repository.name }}"
Gives you ci_success and ci_failure counters plus a ci_ms timing, labelled by repository.
Mark deploys on every chart
- name: Deploy marker
run: curl -s "${{ secrets.PLOTPOINT_URL }}/e/deploy/$(printf '%s' "${{ github.sha }}" | cut -c1-7)"
Python
Standard library only.
import time, urllib.request
U = "https://plotpoint.torvald.no/m/your-account/pick-a-key"
t0 = time.monotonic()
do_the_work()
urllib.request.urlopen(f"{U}/t/job_ms/{(time.monotonic() - t0) * 1000:.0f}?job=nightly")
A small helper that never breaks your program
import urllib.parse, urllib.request
U = "https://plotpoint.torvald.no/m/your-account/pick-a-key"
def pp(kind, metric, value="", **labels):
url = f"{U}/{kind}/{metric}"
if value != "":
url += "/" + urllib.parse.quote(str(value), safe="")
if labels:
url += "?" + urllib.parse.urlencode(labels)
try:
urllib.request.urlopen(url, timeout=2)
except Exception:
pass # metrics must never take the app down
pp("g", "queue_len", 17, worker="a")
pp("c", "emails_sent")
pp("e", "deploy", "v1.4 shipped")
With requests
import requests
requests.get("https://plotpoint.torvald.no/m/your-account/pick-a-key/g/room_temp/21.4", params={"room": "office"}, timeout=2)
Docker
Running containers
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/containers_running/$(docker ps -q | wc -l)?host=$(hostname)"
Unhealthy containers
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/containers_unhealthy/$(docker ps -q --filter health=unhealthy | wc -l)?host=$(hostname)"
Uptime and HTTP checks
Response time from wherever the check runs, and a counter for failures.
#!/bin/sh
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
SITE=https://example.com
ms=$(curl -s -o /dev/null -w '%{time_total}' --max-time 10 "$SITE" | awk '{print int($1*1000)}')
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$SITE")
curl -s "$U/t/http_ms/$ms?site=example"
[ "$code" = 200 ] || curl -s "$U/c/http_fail?site=example&code=$code"
TLS certificate days left
end=$(echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/cert_days/$(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 ))?site=example"
Home and IoT
Raspberry Pi temperature
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/cpu_temp/$(vcgencmd measure_temp | tr -dc 0-9.)?host=pi"
MicroPython (ESP32, Pico W)
import urequests
r = urequests.get("https://plotpoint.torvald.no/m/your-account/pick-a-key/g/humidity/%.1f?room=attic" % sensor.humidity())
r.close()
Home Assistant rest_command
rest_command:
plotpoint:
url: "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/{{ metric }}/{{ value }}?room={{ room }}"
Routers and network
# busybox wget needs to be built with TLS support for https
wget -q -O /dev/null "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/wan_rx_bytes/$(cat /sys/class/net/eth0/statistics/rx_bytes)?host=router"
Ping latency
ms=$(ping -c 3 -q 1.1.1.1 | awk -F/ '/^rtt|^round-trip/ {print int($5)}')
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/t/ping_ms/$ms?target=cloudflare"
JavaScript and Node.js
const U = "https://plotpoint.torvald.no/m/your-account/pick-a-key";
const pp = (kind, metric, value = "", labels = {}) =>
fetch(`${U}/${kind}/${metric}${value === "" ? "" : "/" + encodeURIComponent(value)}?${new URLSearchParams(labels)}`)
.catch(() => {}); // never let metrics break the app
pp("c", "orders");
pp("t", "render_ms", Math.round(performance.now()), { page: "home" });
PowerShell (Windows)
$U = "https://plotpoint.torvald.no/m/your-account/pick-a-key"
$free = [math]::Round((Get-PSDrive C).Free / 1GB, 1)
Invoke-RestMethod "$U/g/disk_free_gb/$($free)?host=$($env:COMPUTERNAME)" | Out-Null
Limits
| Active series per account | 50 (a series is a metric plus its labels; it stays active until 30 days with no writes) |
| Samples per account per day | 10 000 (resets at UTC midnight) |
| Retention | 30 days |
| Request rate per account | 10/s sustained, burst 60 |
| Labels per series | 8, values up to 128 characters |
| Chart resolution | at most 2000 points per response |
Writing a new series past the limit returns 429 with the reason. Nothing is dropped without telling you.
Link previews from chat apps (Slack, Discord, WhatsApp and others) get a 204 but store nothing, so pasting a write URL into a chat can't create an account or record a value.