Monitoring storefront SSL, checkout latency and API health: StatusCake vs DIY scripts
Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.
Monitor four things for an online store: certificate expiry on every hostname (and every server behind it), the response time of the cart and checkout pages, the APIs the storefront depends on, and the monitor itself. A cron script in Python can cover the first three from one server, and our tested version alerts Slack only when something changes. What a script on one server can't do is see your store from several regions or confirm a failure from a second location before paging you, and it can't tell you when its own server dies. StatusCake does both: SSL monitors with expiry reminders, uptime checks with confirmation servers, page speed monitors, and a push (heartbeat) test that can watch your cron script. The free plan covers one SSL monitor and 10 uptime checks at 5-minute intervals; Superior is $20.41 a month billed yearly for 50 SSL monitors and 1-minute checks.
Stores rarely go fully down. What happens instead is that one certificate expires on the API subdomain nobody remembers, one load balancer node keeps an old certificate after a renewal, checkout gets slow for shoppers in one region, or the payment provider's API starts returning errors. The homepage stays up and a basic uptime check stays green. This guide covers what to watch on an online store, shows a tested Python script that watches it from cron, and is specific about where a single script stops being enough and a service like StatusCake earns its price.
StatusCake's plans, intervals, confirmation tests and push monitoring come from its pricing page and knowledge base, and the certificate lifetime schedule from the CA/Browser Forum ballot SC-081 as summarized by several CAs and PKI vendors, all checked in October 2026. The script was tested on a Windows laptop against local TLS servers using certificates from a throwaway test CA (expired, wrong hostname, nearly due, 47-day lifetimes, and one stale node behind a dual-stack name), slow and failing HTTP endpoints, and a mock Slack webhook. It was not run against a live store, and we didn't measure StatusCake's checks ourselves.
1. The short answer
| DIY cron script | StatusCake | |
|---|---|---|
| Certificate expiry | Yes, and every IP behind a name, with your own rules | SSL monitors with reminders; 1 on Free, 50 on Superior |
| Checkout and cart latency | From one server, wherever that is | Uptime and page speed checks from regions you choose |
| API health | Any request you can script, including authenticated POSTs | HTTP checks with status codes and string matching |
| Confirm before alerting | Retries from the same place | 0 to 3 confirmation tests from other servers in the region |
| Who watches the watcher | Nobody, unless you add a heartbeat | Runs outside your infrastructure; push tests can watch your scripts |
| Cost | A server you already have | Free plan; Superior $20.41 a month billed yearly |
Our recommendation is both: the script for checks only you can write, StatusCake for outside-in checks and as the heartbeat that notices when the script stops.
2. What to monitor on a store
| Layer | What to check | Why it fails silently |
|---|---|---|
| Certificates and domain | Storefront, www, checkout, API and webhook subdomains; registration expiry | An expired API certificate breaks a headless checkout while the homepage still loads |
| Shopping path | Product page, cart, checkout: status code, a string on the page, response time | The homepage is cached at the CDN; cart and checkout hit the origin |
| Dependencies | Storefront or REST API, payment and shipping APIs, your webhook relay's /status | Pages render, but orders fail or never reach fulfilment |
| The monitor itself | A heartbeat from the cron job to an outside service | When the monitoring server dies, so do the alerts |
Hosted platforms change the list. On Shopify, certificates for the storefront and checkout are Shopify's job, so your certificate checks belong on what you run yourself: a headless frontend, API and webhook subdomains, a marketing site. On WooCommerce or any self-hosted stack, every hostname is yours.
3. Certificate lifetimes are shrinking
The usual rule, "alert 30 days before expiry", was made for one-year certificates. Publicly trusted certificates are getting shorter on a fixed schedule, by issue date:
A fixed 30-day threshold fires constantly on a 47-day certificate and tells you nothing on a 90-day one, where a healthy setup renews with 30 days left anyway. The useful question is "should this certificate have been renewed by now?" ACME clients like Certbot renew when about a third of the lifetime is left, so the script warns below 30% of the certificate's own lifetime, which means renewal has failed for a few days, and raises a critical alert under 7 days whatever the lifetime. In the test, a 47-day certificate with 20 days left was "ok", and one with 10 days left was a warning.
4. The script
One file, standard library only. Certificates are checked on every address the name resolves to, IPv4 and IPv6. HTTP checks take the median of three requests. Slack gets a message only when a check changes state, problems and recoveries, so a broken certificate doesn't post every 5 minutes for a week.
"""Storefront monitor for cron: certificate expiry on every address behind each hostname, checkout and API
latency, alerts to Slack only when a check changes state. Python 3.10+, standard library only.
Usage: storemon.py checks.json (exit code 2 if anything is critical, for cron mail or a heartbeat)"""
import datetime, json, os, socket, ssl, statistics, sys, time, urllib.error, urllib.request
cfg = json.load(open(sys.argv[1]))
STATE = cfg.get('state_file', os.path.splitext(sys.argv[1])[0] + '.state.json')
ctx = ssl.create_default_context(cafile=cfg.get('ca_file')) # ca_file only for private CAs and tests
def cert_check(target):
"""Every IPv4 and IPv6 address behind the name: one stale node behind a load balancer is enough to break
checkout for some shoppers while a check that resolves one address stays green."""
host, _, port = target.partition(':')
port = int(port or 443)
try:
ips = sorted({ai[4][0] for ai in socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)})
except OSError as e:
return [(target, 'critical', 'DNS lookup failed: %s' % e)]
out = []
for ip in ips:
where = '%s [%s]' % (target, ip)
try:
with socket.create_connection((ip, port), timeout=10) as raw, \
ctx.wrap_socket(raw, server_hostname=host) as tls:
cert = tls.getpeercert()
except ssl.SSLCertVerificationError as e: # expired, wrong name, unknown CA: shoppers see an error page
out.append((where, 'critical', e.verify_message))
continue
except OSError as e:
out.append((where, 'critical', 'TLS connection failed: %s' % e))
continue
start, end = (ssl.cert_time_to_seconds(cert[k]) for k in ('notBefore', 'notAfter'))
left = (end - time.time()) / 86400
lifetime = (end - start) / 86400
# ACME clients renew with about a third of the lifetime left (30 of 90 days). Below 30% the renewal
# has failed for days, whatever the lifetime: 90-day certs today, 47-day certs by 2029.
expires = datetime.datetime.fromtimestamp(end, datetime.UTC).strftime('%Y-%m-%d')
msg = 'expires %s, %.0f of %.0f days left' % (expires, left, lifetime)
out.append((where, 'critical' if left < 7 else 'warning' if left < lifetime * 0.3 else 'ok', msg))
return out
def http_check(c):
"""Median of 3 timed requests, so one slow packet doesn't page anyone. Timed to the response headers."""
times, problem = [], None
for _ in range(3):
req = urllib.request.Request(c['url'], c.get('body', '').encode() or None, c.get('headers', {}),
method=c.get('method', 'GET'))
t = time.perf_counter()
try:
with urllib.request.urlopen(req, timeout=c.get('timeout', 15), context=ctx) as r:
times.append((time.perf_counter() - t) * 1000)
if c.get('expect') and c['expect'] not in r.read(200_000).decode('utf-8', 'replace'):
problem = 'response is missing %r' % c['expect']
except urllib.error.HTTPError as e:
problem = 'HTTP %d' % e.code
except OSError as e:
problem = 'request failed: %s' % getattr(e, 'reason', e)
if problem:
return [(c['name'], 'critical', problem)]
ms = statistics.median(times)
state = 'warning' if ms > c.get('max_ms', 2000) else 'ok'
return [(c['name'], state, 'median %.0f ms (limit %d ms)' % (ms, c.get('max_ms', 2000)))]
results = [r for t in cfg.get('certs', []) for r in cert_check(t)] + \
[r for c in cfg.get('http', []) for r in http_check(c)]
try:
prev = json.load(open(STATE))
except (OSError, ValueError):
prev = {}
changes = [(n, s, m) for n, s, m in results if prev.get(n, 'ok') != s] # new problems and recoveries only
icon = {'critical': ':red_circle:', 'warning': ':large_yellow_circle:', 'ok': ':white_check_mark:'}
if changes and cfg.get('slack_webhook'):
text = '\n'.join('%s *%s*: %s' % (icon[s], n, m) for n, s, m in changes)
req = urllib.request.Request(cfg['slack_webhook'], json.dumps({'text': text}).encode(),
{'Content-Type': 'application/json'})
urllib.request.urlopen(req, timeout=10).close() # if Slack fails, state isn't saved and we retry next run
json.dump({n: s for n, s, _ in results}, open(STATE, 'w'), indent=1)
for n, s, m in results:
print('%-8s %-45s %s' % (s, n, m))
sys.exit(2 if any(s == 'critical' for _, s, _ in results) else 0)
The configuration lists hostnames and requests. The Storefront API check is an authenticated POST that expects a field in the answer, the kind of check that's awkward in most hosted monitors and trivial in a script:
{
"slack_webhook": "https://hooks.slack.com/services/T000/B000/XXXX",
"certs": ["shop.example.com", "www.shop.example.com", "api.shop.example.com", "hooks.shop.example.com"],
"http": [
{"name": "cart", "url": "https://shop.example.com/cart/", "max_ms": 1500},
{"name": "checkout page", "url": "https://shop.example.com/checkout/", "max_ms": 2000},
{"name": "storefront api", "url": "https://shop.example.com/api/2025-10/graphql.json", "method": "POST",
"headers": {"Content-Type": "application/json", "X-Shopify-Storefront-Access-Token": "PUBLIC_TOKEN"},
"body": "{\"query\": \"{ shop { name } }\"}", "expect": "\"name\"", "max_ms": 1000},
{"name": "webhook relay", "url": "https://hooks.shop.example.com/status", "max_ms": 1000}
]
}
Run it from cron. The last part sends a heartbeat to an outside service every time the script ran, whether or not it found problems, so a dead server or a crashing script shows up as missing heartbeats:
# Heartbeat: the push URL your monitoring service gives you (StatusCake push test, Uptime Kuma push monitor)
HEARTBEAT_URL=https://push.example.com/your-unique-url
# Every 5 minutes. Exit 0 = all fine, 2 = something critical (already alerted), 1 = the script crashed:
# ping the heartbeat unless it crashed, so a dead box or a broken script raises an alarm somewhere else
*/5 * * * * cd /opt/storemon && python3 storemon.py checks.json >> storemon.log 2>&1; [ $? -ne 1 ] && curl -fsS -m 10 "$HEARTBEAT_URL" > /dev/null
5. Test results
Each certificate came from a throwaway test CA, served by a local TLS server, with the script's ca_file pointing at that CA:
| Scenario | Expected | Result |
|---|---|---|
| 90-day certificate, renewed 10 days ago (IPv4) | ok | Pass: 80 of 90 days left |
| Same hostname on IPv6, a node that missed the renewal | warning | Pass: 15 of 90 days left |
| Expired certificate | critical | Pass: "certificate has expired" |
| Certificate for a different hostname | critical | Pass: "Hostname mismatch" |
| 47-day certificate, 20 days left | ok (a fixed 30-day rule would warn) | Pass |
| 47-day certificate, 10 days left | warning | Pass |
| Cart: one slow request (1.5 s) out of three | ok, median under the limit | Pass: median 79 ms |
| Checkout at 1.2 s, limit 800 ms | warning | Pass: median 1,226 ms |
| API health returns HTTP 500 | critical, exit code 2 | Pass |
| Second run, nothing changed | No Slack message | Pass |
| API and checkout fixed | One message with two recoveries | Pass |
The IPv6 case is the one worth copying. A check that connects to one address, which is what curl or most quick scripts do, would have reported 80 days left while some visitors were getting a certificate due in 15 days. The same thing happens with several servers behind DNS round-robin, or one node that missed a renewal.
6. Why one location isn't enough
A script on one server sees your store from one network. Three things follow:
- Its problems look like yours. If the server's own network has a bad minute, every check fails at once and you get paged for an outage shoppers never saw. Retrying from the same server doesn't help, because the retry goes through the same network.
- Your regional problems are invisible to it. A CDN edge returning errors in one region, a DNS provider problem in one continent, or checkout taking 4 seconds from Australia while it takes 300 ms from the data center next to your origin: one vantage point shows one of these, at best.
- Its latency isn't your shoppers' latency. A monitor in the same region as the origin measures a best case.
That's the part worth paying for. A script is better at checks that need logic: every IP behind a name, authenticated API calls, lifetime-aware thresholds. An outside service is better at answering "is it down for shoppers, or just for us?" and at still being up when your server isn't.
7. Setting up StatusCake for a store
- SSL monitors on each hostname you're responsible for. Reminders are customizable, with three alerts before expiry; with 90-day or shorter certificates, set them close to expiry, for example 21, 10 and 3 days, so they mean "renewal failed" rather than "normal life cycle".
- Uptime tests on the product page, cart and checkout, with a string match on something that only appears when the page works, like the checkout button label, not just a 200. Add
/statusof your webhook relay or other backend services. - Confirmation tests set to 1 to 3, so one bad network path doesn't page anyone.
- Page speed monitors on the cart and checkout, for trends rather than alerts: every 15 minutes on Superior, every 5 on Business.
- A push test for the cron script: StatusCake gives you a URL, and alerts if no push arrives within the period you set. Push monitoring is included on free and paid plans.
- Domain monitors for registration expiry. An expired domain takes everything down at once, and renewal emails go to whoever registered it years ago.
8. What StatusCake costs
| Plan | Price | Uptime | SSL | Page speed | Domains |
|---|---|---|---|---|---|
| Free | $0 | 10 monitors, every 5 min | 1, checked daily | 1, daily | 1 |
| Superior | $20.41/mo yearly, $24.49 monthly | 100, every 1 min | 50, every 30 min | 15, every 15 min | 50 |
| Business | $66.66/mo yearly, $79.99 monthly | 300, every 30 s | 100, every 10 min | 30, every 5 min | 120 |
Prices from StatusCake's pricing page, October 2026. For one store, Free covers the uptime checks and the heartbeat, but only one SSL monitor; Superior is the plan where every hostname gets its own SSL monitor and 1-minute checks on checkout.
9. Our opinion: trade-offs
- Don't choose; split the work. Run the script for logic-heavy checks (every IP, authenticated APIs, lifetime-aware certificate rules) and use StatusCake for outside-in uptime, confirmation and the heartbeat. The script's blind spots are exactly what an outside service covers.
- Alert on state changes, not on state. A monitor that repeats itself every 5 minutes gets muted, and then it's worse than nothing.
- Thresholds should follow certificate lifetime. 30 days made sense for one-year certificates. With 90 days now and 47 days by 2029, alert on "renewal is overdue", not on a fixed number of days.
- Measure the path that makes money. Homepage checks catch the least interesting failures. Cart, checkout and the APIs behind them are where a slowdown costs orders.
- Why StatusCake is our pick here. It covers SSL, domain, uptime with confirmation, page speed and push heartbeats in one place, and the free plan is enough to start with the heartbeat and the most important uptime checks.
Related: UptimeRobot vs custom scripts for checkout and scraper monitoring, hosting a webhook relay on Railway or a VPS and protecting WooCommerce checkout from carding bots.
10. Tools
Top picks
| Tool | Role | Why it fits |
|---|---|---|
| Outside-in monitoring and the heartbeat for your scripts | Uptime tests with confirmation servers, SSL and domain monitors, page speed monitors and push tests. Free plan with 10 uptime monitors; Superior at $20.41 a month billed yearly. |
Alternatives: free and official tools
We don't earn anything from these links.
- Uptime Kuma: open-source, self-hosted monitoring with HTTP keyword checks, certificate expiry notices, push monitors and Slack alerts. It shares the single-location limit, so run it somewhere other than the servers it watches.
- Python
sslmodule:getpeercert()andcert_time_to_seconds(), used by the script. - Let's Encrypt integration guide: renewal timing and why to renew automatically, well before expiry.
- crt.sh: Certificate Transparency search; lists every certificate issued for your domains, including hostnames you forgot about.
- Slack incoming webhooks: how to get the URL the script posts to.
11. Pitfalls
- Checking one IP per hostname. A stale node behind a load balancer or on IPv6 stays invisible.
- Disabling verification to read the expiry date. A check with verification off reports "60 days left" on a certificate browsers reject for the wrong hostname or an incomplete chain.
- A fixed 30-day threshold. Noisy on short certificates, late on long ones. Use a share of the lifetime plus a hard floor.
- Monitoring only the homepage. It's usually cached at the CDN and the last thing to fail.
- The monitor on the server it monitors. When the server dies, so do the alerts. Add a heartbeat to an outside service.
- Alerting every run. Repeated alerts get muted. Alert on changes, including recoveries.
- Forgetting the domain itself. Registration expiry takes down the store, email and every certificate renewal at once.
Watch certificates on every hostname and every IP behind it, with thresholds tied to the certificate's lifetime, time the cart, checkout and APIs, and alert only on changes. A standard-library Python script does that from cron and passed every scenario in our test. What it can't do is confirm a failure from another location or notice its own death. StatusCake covers both, with confirmation tests, SSL and domain monitors, and a push test that watches the script, and its free plan is enough to start.