Backend error notifications
The cardinal rule of a production backend: an unhandled error should knock on the door. Below are compact wrappers and middleware for three popular stacks.
General rules
Section titled “General rules”- Do not block the request by sending a notification. Do
awaitin the background or fire-and-forget. - Throttling. If the backend receives 1000 identical errors per minute — don’t turn Notifly into spam. Minimal protection: send no more than one notification per N seconds for the same key (stack hash).
- Don’t send secrets. Sanitize the request body of passwords and tokens.
Node.js (Express)
Section titled “Node.js (Express)”const fetch = require('node-fetch');
const seen = new Map();const COOLDOWN_MS = 60_000;
async function notify(title, message, priority = 7) { const key = title + message.slice(0, 200); if (Date.now() - (seen.get(key) || 0) < COOLDOWN_MS) return; seen.set(key, Date.now());
try { await fetch(`${process.env.NOTIFLY_URL}/message?token=${process.env.NOTIFLY_TOKEN}`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title, message, priority}), timeout: 5000, }); } catch (e) { console.error('notifly failed:', e.message); }}
module.exports = {notify};Integrate into Express:
const express = require('express');const {notify} = require('./notifly');const app = express();
// your routes ...
// last middleware — catch all errorsapp.use((err, req, res, next) => { notify( `🐛 ${req.method} ${req.path} — ${err.name}`, `${err.message}\n\n${err.stack?.slice(0, 1500)}`, 8 ); res.status(500).json({error: 'internal'});});
// catch things that crashed outside a request contextprocess.on('unhandledRejection', (reason) => { notify('💥 unhandledRejection', String(reason).slice(0, 1500), 9);});process.on('uncaughtException', (err) => { notify('💥 uncaughtException', `${err.message}\n${err.stack}`, 10);});Python (FastAPI)
Section titled “Python (FastAPI)”import os, time, httpxfrom collections import OrderedDict
_COOLDOWN = 60_seen: "OrderedDict[str, float]" = OrderedDict()
def notify(title: str, message: str, priority: int = 7) -> None: key = title + message[:200] now = time.time() if now - _seen.get(key, 0) < _COOLDOWN: return _seen[key] = now if len(_seen) > 200: _seen.popitem(last=False) try: httpx.post( f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5, ) except Exception as e: print(f"notifly failed: {e}")FastAPI exception handler:
from fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponseimport tracebackfrom notifly import notify
app = FastAPI()
@app.exception_handler(Exception)async def all_exceptions(request: Request, exc: Exception) -> JSONResponse: notify( title=f"🐛 {request.method} {request.url.path} — {type(exc).__name__}", message=f"{exc}\n\n{traceback.format_exc()[-1500:]}", priority=8, ) return JSONResponse({"error": "internal"}, status_code=500)Go (net/http)
Section titled “Go (net/http)”package notifly
import ( "bytes" "encoding/json" "net/http" "os" "sync" "time")
var ( seen = map[string]time.Time{} seenLock sync.Mutex cooldown = 60 * time.Second client = &http.Client{Timeout: 5 * time.Second})
func Notify(title, message string, priority int) { key := title if len(message) > 200 { key += message[:200] } else { key += message } seenLock.Lock() if time.Since(seen[key]) < cooldown { seenLock.Unlock() return } seen[key] = time.Now() seenLock.Unlock()
body, _ := json.Marshal(map[string]any{ "title": title, "message": message, "priority": priority, }) url := os.Getenv("NOTIFLY_URL") + "/message?token=" + os.Getenv("NOTIFLY_TOKEN") go func() { resp, err := client.Post(url, "application/json", bytes.NewReader(body)) if err == nil { resp.Body.Close() } }()}Middleware for net/http:
import ( "fmt" "net/http" "runtime/debug")
func Recovery(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { stack := string(debug.Stack()) if len(stack) > 1500 { stack = stack[:1500] } Notify( fmt.Sprintf("🐛 %s %s — panic", r.Method, r.URL.Path), fmt.Sprintf("%v\n\n%s", rec, stack), 9, ) http.Error(w, "internal", 500) } }() next.ServeHTTP(w, r) })}What to send besides errors
Section titled “What to send besides errors”- Service start. ”🚀 backend v1.42 started on host-A” — the deployment is immediately visible.
- DB health degradation (timeout > N ms) — separate priority.
- Suspicious security events. For example, an authentication attempt from an unknown User-Agent for an admin account.
- Business events. First payment from a new client, quota exceeded.
Benefits
Section titled “Benefits”- Knowing about 500s within a minute, not an hour later from a user complaint.
- Context in your pocket. Method, path, short stack trace — usually enough to open your IDE and start fixing.
- Throttling built-in — no 10,000 notifications due to a broken DB pool.