In this episode we'll turn aria2 into an automation engine: reading exit codes, calling aria2c from shell scripts and cron, batch downloads with loops and xargs, JSON-RPC integration from Python, Node, and Go, and event notification with --on-download-complete.

In episode 17 you extracted maximum speed from aria2 — multi-connection with -x and -s, max-concurrent-downloads, even disk cache tuning. But speed only matters if downloads can run unattended, be repeated many times, and be integrated into a larger workflow. This episode 18 changes aria2 from a tool into an automation engine.
The difference isn't the options used, but how you treat it: as a program that returns exit codes, can be called from scripts and cron, is controlled from applications via RPC, and tells you when a download finishes. Let's build it all.
Every time aria2c finishes, it exits with an exit code — a number that's the only language scripts can reliably read. Unlike curl, which handles one request, aria2 handles many downloads at once, so its exit code reflects the status of the whole session, not individual files.
aria2c --dir=/srv/downloads https://cdn.example.com/update.iso
echo "aria2 keluar dengan kode: $?"| Code | Meaning |
|---|---|
| 0 | All downloads succeeded |
| 1 | Unknown error |
| 2 | Timeout |
| 3 | Resource not found |
| 4 | Too many "resource not found" errors (per max-file-not-found) |
| 5 | Download aborted because speed was too slow |
| 6 | Network problem |
| 9 | Not enough disk space |
| 19 | Name resolution failed (DNS) |
| 24 | HTTP authentication failed |
| 28 | Unrecognized option or wrong argument |
| 32 | Checksum verification failed |
Tip
Remember one weakness of aria2's exit codes: an error on an already finished download isn't reported — the exit code only reflects the last error that occurred. If you need per-file certainty, verify outside aria2 (e.g. checksums) or read its logs. This detail will matter in episodes 19 and 21.
The first principle of automation: never ignore the exit code. If a download is a prerequisite for the next step — for example a database that must exist before an application is installed — the script must stop when the download fails, not continue with incomplete data.
if aria2c --dir=/srv/downloads --max-tries=3 \
https://cdn.example.com/update.iso; then
echo "Download selesai, lanjut instalasi"
./install.sh
else
echo "Download gagal, hentikan pipeline" >&2
exit 1
fiThe if aria2c ...; then pattern is what turns aria2 into a logic block: success or failure determines the next flow. In cron, the same behavior decides whether a job counts as successful or triggers an alert — cron reads the script's exit code as the success signal.
Automation rarely deals with a single URL — usually dozens or hundreds. Two patterns are most common: loops for per-URL control (exit codes checked one by one), and xargs for parallel processing. The -P 4 flag runs up to 4 aria2c processes at once:
while read -r url; do
[ -z "$url" ] && continue
if aria2c --dir=/srv/downloads "$url"; then
echo "OK: $url"
else
echo "GAGAL: $url" >&2
fi
done < urls.txtBe careful with this combination: each aria2c process can open many connections per file (from episodes 4 and 17). Four processes with -x 16 means up to 64 connections to the same server — enough to make the server block you. For batches against one host, lower -x per process, or better, hand the queue to the RPC daemon (below) so connections are managed globally.
To build a personal download manager, run aria2 as an RPC daemon (from episode 15) and send JSON-RPC requests to port 6800. The advantages: the daemon keeps running, queues and progress are managed by aria2, and the application just sends methods like aria2.addUri. The JSON-RPC payload looks like:
{
"jsonrpc": "2.0",
"id": "1",
"method": "aria2.addUri",
"params": [
"token:RAHASIA",
["https://cdn.example.com/dataset.zip"],
{ "dir": "/srv/downloads" }
]
}Notice the RPC secret is sent as the first params element with the token: prefix. Every method follows the same pattern: start params with the token, then the method arguments. Now call the http://localhost:6800/jsonrpc endpoint from your favorite language — these three examples only need the standard library:
import json
import urllib.request
payload = {
"jsonrpc": "2.0",
"id": "1",
"method": "aria2.addUri",
"params": [
"token:RAHASIA",
["https://cdn.example.com/dataset.zip"],
{"dir": "/srv/downloads"}
]
}
req = urllib.request.Request(
"http://localhost:6800/jsonrpc",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
print(resp.read().decode())All three examples do exactly the same thing: send aria2.addUri with the secret and download options. The response contains a GID — the download's unique identifier — which you can save to monitor progress via aria2.tellStatus(gid) or stop it with aria2.remove(gid). This is the core of a personal download manager: queue, pause, resume, and notifications can all be built in your own language.
Good automation doesn't wait — it listens. aria2 has an event hook: a script run automatically when a certain event happens. The most useful is --on-download-complete, called with three arguments: the GID, the number of files, and the path of the first file.
#!/usr/bin/env bash
GID="$1"
NUM_FILES="$2"
FILE_PATH="$3"
LOG_FILE="/var/log/aria2-hooks.log"
FINAL_DIR="/srv/downloads/selesai"
mkdir -p "$FINAL_DIR"
echo "[$(date +%F_%T)] Selesai: $GID - $FILE_PATH" >> "$LOG_FILE"
mv "$FILE_PATH" "$FINAL_DIR/"Attach the hook to a download:
aria2c --dir=/srv/downloads \
--on-download-complete /opt/hooks/on-complete.sh \
https://cdn.example.com/file.zipThis hook is the bridge between aria2 and the outside world: moving files, triggering checksum verification, calling an API, or sending notifications. For failed downloads there's --on-download-error, and for anything that stops — success or failure — there's --on-download-stop. In episode 21 we'll use these hooks to build a complete download workflow.
Tip
When using hooks in cron or a daemon, make sure the script is executable (chmod +x /opt/hooks/on-complete.sh) and use an absolute path in --on-download-complete. A hook that can't run only appears as an error in the log — the download is still counted as complete, so this trap is easy to miss.
Episode 18 turned aria2 into an automation engine: understanding exit codes as the language of session failures, calling aria2c from shell scripts with the if ...; then pattern, running batch downloads with loops and xargs -P, building your own download manager via JSON-RPC from Python, Node, and Go, and listening for events with --on-download-complete.
The key takeaway: scripts don't read screens — they read exit codes and events. Everything that looks like manual work can become a callable function running unattended.
In the next episode, episode 19, we flip the point of view: when that automation fails, how do you read the evidence? Troubleshooting and debugging — from --log and --log-level to network, TLS, and BitTorrent errors. See you then!