Bash and Linux

Health-check a list of URLs and exit non-zero on failure

mediumSmall scripts Must-do

Problem statement

After a deploy, a pipeline step must check every endpoint in urls.txt and fail the build if any of them is unhealthy. Write healthcheck.sh <url-file> that skips blank lines and # comments, requests each URL with a 5 second timeout, prints OK or FAIL with the HTTP status for each, prints how many checks failed, and exits 1 if any check failed (0 otherwise). A 2xx status counts as healthy. For the example, assume api and web return 200, legacy returns 503, and old.example.internal does not resolve (curl reports status 000).

urls.txt

Bash
# production endpoints
https://api.example.com/healthz
https://web.example.com/
https://legacy.example.com/status
https://old.example.internal/

Examples

Example 1

Input: `./healthcheck.sh urls.txt; echo "exit: $?"` (under the conditions above)

Output: OK 200 https://api.example.com/healthz OK 200 https://web.example.com/ FAIL 503 https://legacy.example.com/status FAIL 000 https://old.example.internal/ 2 check(s) failed exit: 1

Explanation: If every endpoint returned 2xx, the script would print 0 check(s) failed and exit 0.

Hints

Approach

Optimal

${1:?...} aborts with a usage message if no file is given. while IFS= read -r url reads one line at a time without trimming spaces or interpreting backslashes, and || [ -n "$url" ] still processes a last line that has no trailing newline. Blank lines and comments are skipped with continue. curl -s -o /dev/null -w '%{http_code}' --max-time 5 discards the body, prints just the status code, and gives up after 5 seconds; on DNS or connection errors curl prints 000. The regex ^2[0-9][0-9]$ accepts any 2xx. A counter tracks failures, and the final [ "$failed" -eq 0 ] is the last command, so its status (0 or 1) becomes the script's exit code, which is what fails the pipeline.

Bash
#!/usr/bin/env bash
# Usage: healthcheck.sh <url-file>
set -uo pipefail
file=${1:?Usage: healthcheck.sh <url-file>}
failed=0
while IFS= read -r url || [ -n "$url" ]; do
# skip blank lines and comments
[[ -z "$url" || "$url" == \#* ]] && continue
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url")
if [[ "$code" =~ ^2[0-9][0-9]$ ]]; then
echo "OK $code $url"
else
echo "FAIL $code $url"
failed=$((failed + 1))
fi
done < "$file"
echo "$failed check(s) failed"
[ "$failed" -eq 0 ]

Follow-up questions

  • Check the URLs in parallel so 50 endpoints do not take 50 x 5 seconds. (xargs -P, background jobs with wait)
  • Add retries with a delay before declaring an endpoint failed, and follow redirects with -L.

Frequently asked questions

With set -e, the first failing curl would end the script before the remaining URLs are checked, and a failing [[ ]] test can also trigger an exit in some constructs. A health check should report every endpoint, so failures are counted explicitly and the exit code is set once at the end.

By default curl exits 0 for any HTTP response, including 500, because the transfer itself worked. -f makes it exit 22 on 4xx/5xx, but you lose the status code in the message. Reading %{http_code} covers both cases: 000 for network errors and the real code otherwise.