appscript.dev
Automation Beginner Sheets Gmail

Monitor website uptime and response time

Ping Northwind URLs every 10 minutes and alert on failures — DIY uptime monitor.

Published Sep 1, 2025

A paid uptime monitor is the obvious answer when a site goes down, but for a small studio it is one more subscription and one more dashboard to log into. Northwind only runs two URLs — the marketing site and the app — and just wants to know, fast, when either stops responding.

This script does exactly that and nothing more. Every 10 minutes it fetches each URL, records the status code and how long the request took, and emails the team the moment something returns an error. The log of response times also doubles as a rough performance history you can skim when the app feels slow.

What you’ll need

  • A Google Sheet to act as the log. The script writes four columns — timestamp, URL, status code, response time in milliseconds — so an empty sheet with no headers is fine; add a header row yourself if you like.
  • The spreadsheet’s ID, taken from its URL, dropped into UPTIME_SHEET_ID.
  • The list of URLs you want watched, in SITES.
  • An address to send alerts to, in ALERT_RECIPIENT. The account running the script must be able to send mail as itself.

The script

// The spreadsheet that stores the uptime log.
const UPTIME_SHEET_ID = '1abcUptimeId';

// The URLs to check. Add or remove entries freely.
const SITES = ['https://northwind.studio', 'https://app.northwind.studio'];

// Where failure alerts are sent.
const ALERT_RECIPIENT = '[email protected]';

// Any HTTP status at or above this counts as a failure.
const ERROR_STATUS = 400;

/**
 * Fetches every site in SITES, logs the result, and emails an alert
 * if any site returns an error or fails to respond.
 */
function checkSites() {
  const sheet = SpreadsheetApp.openById(UPTIME_SHEET_ID).getSheets()[0];

  // Collect a human-readable line for every site that fails this run.
  const failures = [];

  for (const url of SITES) {
    // 1. Time the request so we capture response time, not just status.
    const start = Date.now();
    try {
      // 2. muteHttpExceptions lets us read 4xx/5xx responses instead
      //    of throwing — a 503 is data, not a crash.
      const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
      const ms = Date.now() - start;
      const code = res.getResponseCode();

      // 3. Log the timestamp, URL, status code and response time.
      sheet.appendRow([new Date(), url, code, ms]);

      // 4. Anything 400+ is a failure worth alerting on.
      if (code >= ERROR_STATUS) failures.push(url + ' → ' + code);
    } catch (e) {
      // 5. A thrown error means the request never completed — DNS
      //    failure, timeout, connection refused. Log and alert on it.
      sheet.appendRow([new Date(), url, 'error', e.message]);
      failures.push(url + ' → ' + e.message);
    }
  }

  // 6. One email per run, listing every site that failed.
  if (failures.length) {
    GmailApp.sendEmail(
      ALERT_RECIPIENT,
      'Site(s) down',
      failures.join('\n')
    );
  }
}

How it works

  1. checkSites opens the log spreadsheet and grabs its first tab.
  2. For each URL in SITES, it records the start time so it can measure how long the request takes.
  3. It fetches the URL with muteHttpExceptions: true, which means a 404 or 503 comes back as a normal response object instead of throwing — that lets the script read and log the status code rather than crashing.
  4. It appends a row with the timestamp, URL, status code and response time in milliseconds.
  5. Any status code of 400 or above is added to the failures list. If the fetch throws entirely — a DNS failure, a timeout, a refused connection — the catch block logs error with the message and adds that to failures too.
  6. After all sites are checked, if failures is not empty it sends a single email listing every site that went wrong this run.

Example run

On a healthy run with both sites up, the log gains two rows and no email is sent:

TimestampURLStatusResponse (ms)
2026-05-24 14:00https://northwind.studio200312
2026-05-24 14:00https://app.northwind.studio200488

If the app returns a server error on the next run, the log records it and the team gets an email:

TimestampURLStatusResponse (ms)
2026-05-24 14:10https://northwind.studio200305
2026-05-24 14:10https://app.northwind.studio503121

Subject: Site(s) down Body: https://app.northwind.studio → 503

Trigger it

This automation only earns its keep on a schedule:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose checkSites, event source Time-driven, type Minutes timer, interval Every 10 minutes.
  4. Save and approve the authorisation prompt.

Ten minutes is a sensible default — frequent enough to catch real outages, infrequent enough to stay well inside quota.

Watch out for

  • A 10-minute interval means up to 144 runs a day, each making one fetch per site. With two sites that is roughly 288 fetches daily — comfortably under the 20,000-per-day UrlFetchApp limit, but watch it if you add many URLs.
  • A passing status code is not a passing page. A site can return 200 while serving a broken page or a maintenance notice. For deeper checks, fetch the body and search it for a string you expect to see.
  • One blip should not always page the team. If brief slowdowns cause noise, only alert when a site fails two runs in a row by checking the last logged status before sending.
  • The log grows by a few rows every run. Trim or archive it periodically, or point new rows at a fresh tab each month, so the sheet stays quick to open.
  • Response time includes Google’s own network latency to your server, so treat the millisecond figures as a relative trend, not an exact benchmark.

Related