Free, open-source Render uptime monitoring. Add your Render free-tier URLs, pick an interval, and never let them spin down again.
Built by Shyamnath Sankar · MIT licensed
- What this is
- How it works
- Features
- Tech stack
- Project structure
- Local development
- Firebase setup (step by step)
- Firestore security rules
- Environment variables
- Deploy the frontend → Vercel (step by step)
- Deploy the backend → Render (step by step)
- Post-deploy verification
- Updating a deployed app
- Scaling & concurrency
- Troubleshooting
- SEO
- Support
- License
RenderPing is a keep-alive and uptime monitor built specifically for Render free-tier services.
Render's free plan spins down any service that receives no traffic for 15 minutes. When a user hits a spun-down service, they wait 5–30+ seconds for a cold start — and often leave. RenderPing solves this by sending a lightweight HTTP request (GET) to your endpoints on a schedule you choose, so the spin-down timer never triggers.
The repo contains two parts that talk to the same Firebase project:
| Part | Location | What it does |
|---|---|---|
| Frontend | /src (Next.js) |
Landing page + real-time dashboard. Sign-in via Firebase Auth, jobs/logs read/write via Firestore with live onSnapshot updates. |
| Backend pinger | /backend (Node.js) |
Standalone worker that subscribes to the jobs collection, fires HTTP pings on each job's schedule, and writes results back to logs. Uses the Firebase Admin SDK (bypasses security rules). |
Key mental model: the frontend is only a control panel. The backend worker does the actual pinging. If the backend isn't running, nothing gets pinged.
You add a monitor on the dashboard
│
▼
Firestore jobs/{jobId} (saved by the frontend)
│
▼
Backend pinger (subscribes to `jobs` via onSnapshot, one setInterval per job)
│ pings every N seconds (default 5 min)
▼
Your Render service receives a GET request → spin-down timer resets
│
▼
Backend writes result atomically (batched):
logs/{logId} + jobs/{jobId} (lastPingAt, lastStatus, latency…)
│
▼
Dashboard updates live via onSnapshot — no polling, no WebSockets
Success criteria: the pinger counts a ping as successful when the server returns any HTTP response (200, 404, 405, 500 — if it answered, it's alive). A ping only fails on network-level errors: timeout, connection refused, or DNS failure. This keeps the "consecutive failures" counter meaningful: it only climbs when your server is genuinely unreachable.
- Landing page — hero, features, how-it-works with code samples, FAQ, CTA, Buy-me-a-coffee support section, footer. Light + dark themes.
- Auth — email/password + Google sign-in via Firebase Auth, per-user profile docs.
- Dashboard — overview stats (active monitors, uptime %, avg latency, errors), 24h latency chart, live activity feed, monitor list with search, per-monitor detail with ping history, settings.
- Monitor management — up to 10 monitors per free account, any URL, interval from 30s to 1h (5 min default). Pause / resume / edit / delete inline.
- Real-time — every ping lands in the dashboard instantly via Firestore
onSnapshot. - Failure tracking — 3 consecutive network failures flips a monitor to
error(red). Success resets the counter. - Log pruning — backend auto-deletes logs older than 7 days (
WARDEN_LOG_RETENTION_DAYS). - Efficient pinger — batched Firestore writes (1 write per ping instead of 2), config-change detection so its own updates never trigger re-pings.
- Demo mode — without Firebase env vars, the dashboard renders realistic sample data so you can preview the UI first.
- Frontend: Next.js (App Router), React 19, TypeScript, Tailwind CSS, shadcn/ui, Recharts, next-themes
- Auth + DB: Firebase Authentication, Cloud Firestore (real-time)
- Backend: Node.js 20+,
firebase-adminv14, nativefetch
renderping/
├── src/ # Frontend (Next.js)
│ ├── app/
│ │ ├── layout.tsx # Root layout, SEO metadata + JSON-LD schemas
│ │ ├── page.tsx # Single route — landing ↔ dashboard
│ │ ├── manifest.ts # PWA web manifest
│ │ ├── robots.ts # robots.txt
│ │ ├── sitemap.ts # sitemap.xml
│ │ ├── opengraph-image.tsx # Dynamic OG image
│ │ └── twitter-image.tsx # Dynamic Twitter card
│ ├── components/
│ │ ├── landing/ # Navbar, Hero, Features, HowItWorks, Support, FAQ, CTA, Footer
│ │ ├── dashboard/ # Sidebar, StatsCards, LatencyChart, JobList, JobFormDialog,
│ │ │ # RecentLogs, JobDetail, SettingsPanel, DashboardShell
│ │ ├── auth/auth-modal.tsx # Sign in / sign up / demo
│ │ ├── kofi-button.tsx # Buy me a coffee button
│ │ └── ui/ # shadcn/ui primitives
│ ├── hooks/
│ │ ├── use-auth.tsx # Firebase Auth context
│ │ ├── use-jobs.ts # Firestore onSnapshot for jobs
│ │ └── use-logs.ts # Firestore onSnapshot for logs
│ └── lib/
│ ├── firebase.ts # Client SDK init (graceful demo fallback)
│ ├── types.ts # Shared types, intervals, limits
│ ├── mock-data.ts # Demo-mode sample data
│ └── dashboard-utils.ts
├── backend/ # Pinger worker (Node.js)
│ ├── src/
│ │ ├── index.js # Entry — boots Firebase, subscribes to jobs
│ │ ├── firebase.js # Admin SDK init (file OR SERVICE_ACCOUNT_JSON env)
│ │ ├── scheduler.js # One setInterval per job, config-change aware
│ │ ├── pinger.js # HTTP ping + batched Firestore write
│ │ └── utils.js # Logger
│ ├── package.json
│ └── service-account.example.json
├── .env # Frontend Firebase config (see below)
└── package.json
Works on Windows, macOS, and Linux. Requires Node.js 20+ (or Bun) and an internet connection.
# from the repo root
npm install
npm run dev # http://localhost:3000Without Firebase env vars the app boots in demo mode (sample monitors/logs, no database). The dev script is Windows-friendly (no Unix tee).
cd backend
npm install
# Option A — local file (simplest):
copy service-account.example.json service-account.json
# → paste your real Firebase service account into service-account.json
# Option B — env var (same mechanism as Render):
# set SERVICE_ACCOUNT_JSON to the full JSON contents
npm run devYou should see:
[warden] firebase project: your-project-id
[warden] loaded 0 job(s) from Firestore
[warden] ready ✓
Create a monitor on the frontend and the pinger picks it up within seconds, pinging on the interval you chose.
You need one Firebase project for both the frontend and the backend. Total time: ~15 minutes.
- Go to https://console.firebase.google.com
- Click Add project → name it (e.g.
render-wardon) → continue - Disable Google Analytics (not used) → Create project
- Wait ~30 seconds for provisioning.
- Left sidebar → Build → Authentication → Get started
- Sign-in method tab:
- Email/Password → toggle Enable → Save
- Google → toggle Enable → choose a support email → Save
- Settings tab → Authorized domains — make sure these are present (add any that aren't):
localhost(dev)your-app.vercel.app(your Vercel domain after deployment)- any custom domain you use
- Build → Firestore Database → Create database
- Choose Production mode (you'll paste rules next)
- Pick a region (any — pick one close to your Render pinger)
- Wait ~1 minute for provisioning.
- Firestore Database → Rules tab
- Paste the rules from Firestore security rules below
- Click Publish.
- Gear icon → Project settings
- Scroll to Your apps → click the Web (
</>) icon - Nickname:
renderping-web→ Register app - Copy the
firebaseConfigobject values — you'll need them for the frontend env vars. (Skip the SDK install steps; the app already uses the npm SDK.) - Optional: Build → Analytics → Get started if you want
measurementIdto work.
- Project settings → Service accounts tab
- Click Generate new private key → Generate key
- A JSON file downloads. This is a secret — never commit it.
- You'll use it either locally (as
backend/service-account.json) or in production (paste into theSERVICE_ACCOUNT_JSONenv var on Render).
# root:
npm run dev
# backend folder (second terminal):
cd backend && npm run devSign up on the dashboard → Settings should not show connection warnings → add a monitor → the backend logs ping ok.
No composite indexes needed. Queries filter by
userId/jobIdonly and sort client-side, so Firestore's auto-created single-field indexes are enough.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
match /jobs/{jobId} {
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
match /logs/{logId} {
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
allow write: if false; // only the Admin SDK (backend) writes logs
}
}
}- Users can only read/write their own docs.
- The backend uses the Admin SDK, which bypasses rules entirely — that's why it can write
logsand updatejobs.
| Variable | Required | Example |
|---|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY |
✅ | AIzaSyBfDQ7oCkCvetH_DsfMNqSKumud8cNDtks |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN |
✅ | render-wardon.firebaseapp.com |
NEXT_PUBLIC_FIREBASE_PROJECT_ID |
✅ | render-wardon |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET |
✅ | render-wardon.firebasestorage.app |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID |
✅ | 648345582359 |
NEXT_PUBLIC_FIREBASE_APP_ID |
✅ | 1:648345582359:web:… |
NEXT_PUBLIC_FIREBASE_DATABASE_URL |
❌ | Realtime DB URL (unused — app uses Firestore) |
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID |
❌ | G-… (enables Analytics) |
| Variable | Required | Default | Description |
|---|---|---|---|
SERVICE_ACCOUNT_JSON |
✅ (prod) | — | Full contents of your service-account.json |
FIREBASE_PROJECT_ID |
❌ | from JSON | Override project id |
FIRESTORE_DATABASE_URL |
❌ | — | Only for non-default Firestore databases |
WARDEN_LOG_LEVEL |
❌ | info |
debug / info / warn / error |
WARDEN_LOG_RETENTION_DAYS |
❌ | 7 |
Auto-delete logs older than N days |
WARDEN_REQUEST_TIMEOUT_MS |
❌ | 10000 |
Per-ping HTTP timeout |
WARDEN_USER_AGENT |
❌ | RenderWarden/1.0 |
UA string sent with pings |
-
Push your repo to GitHub (if not done):
git init git add . git commit -m "Initial commit" git branch -M main git remote add origin https://github.com/<you>/<repo>.git git push -u origin main
.gitignorealready excludesbackend/service-account.json,node_modules, and.env*. The root.envholds only public web keys — safe to commit, but set the same values as Vercel env vars anyway. -
Go to https://vercel.com → Add New… → Project → Import your GitHub repo.
-
Vercel auto-detects Next.js — don't change the framework preset. Build command stays
npm run build(runs on Linux, where thecpsteps work). -
Environment Variables (Project → Settings → Environment Variables), add:
NEXT_PUBLIC_FIREBASE_API_KEY NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN NEXT_PUBLIC_FIREBASE_PROJECT_ID NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID NEXT_PUBLIC_FIREBASE_APP_ID NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID (optional)Apply to all environments (Production + Preview).
-
Click Deploy. After ~2 minutes you get
https://<project>.vercel.app. -
Firebase: Authentication → Settings → Authorized domains → add
https://<project>.vercel.app(domain only:<project>.vercel.app). -
Optional: Project → Settings → Domains to connect a custom domain, then add it to Firebase Authorized domains too (and the Google OAuth redirect
https://yourdomain/__/auth/handlerif Google sign-in misbehaves).
⚠️ The pinger must never sleep. Render's free instances spin down after 15 minutes of inactivity — which would stop all your pings. Use a Background Worker on the Starter plan (or any always-on host: Railway, Fly.io, a VPS).
-
Push your repo to GitHub (see Vercel step 1 — same repo).
-
Go to https://render.com → New → Background Worker.
-
Connect repository → pick your GitHub repo.
-
Configure the service:
Setting Value Name renderping-pingerRoot Directory backendEnvironment NodeBuild Command npm installStart Command npm startInstance Type Starter (paid — free spins down) -
Environment variables — add:
SERVICE_ACCOUNT_JSON= paste the entire contents of your service-account.json (multi-line paste; Render stores it as a secret).- Optional:
WARDEN_LOG_LEVEL=info,WARDEN_LOG_RETENTION_DAYS=7,WARDEN_REQUEST_TIMEOUT_MS=10000.
-
Click Create Background Worker → it builds and starts automatically (~2 min).
-
Open the Logs tab. You should see:
[warden] firebase project: render-wardon [warden] booting… [warden] loaded N job(s) from Firestore [warden] ready ✓Then, after you create a monitor:
ping ok GET https://… 200 289mson every interval.
Deploying elsewhere instead? Same idea on Railway (
railway up), Fly.io (fly deploy), or any VPS (pm2 start src/index.js): root dirbackend/,npm install,npm start, and provideSERVICE_ACCOUNT_JSONor aservice-account.jsonfile.
- ✅ Open
https://<project>.vercel.app— landing page loads, no demo-mode notice. - ✅ Sign up with email or Google — works (domain is authorized).
- ✅ Create a monitor for your Render service → within seconds the Render logs show
new job detected+scheduling "…" every 300s. - ✅ Wait one interval →
ping oklines appear; dashboard shows green status + latency. - ✅ Check the dashboard in another browser/tab — updates arrive live.
- ✅ Kill your Render service → within 3 pings the monitor flips to red
error.
Frontend: push to main → Vercel auto-redeploys. Or Production Deployments → Redeploy.
Backend: push to main → Render auto-redeploys (watch the Logs tab). To restart manually: Manual Deploy → Deploy latest commit. To update secrets: Environment → edit SERVICE_ACCOUNT_JSON → Save → Redeploy.
The pinger is a single Node instance holding one setInterval per job, with a Firestore subscription for live changes.
| Load | Expectation |
|---|---|
| ~10 monitors (1/min) | Free Spark tier is fine (20K writes/day) |
| ~100 monitors (5 min) | One small instance handles it; ~28K writes/day — Blaze recommended |
| ~1,000 monitors (5 min) | ~3.3 pings/sec, trivial for one instance; |
| 10,000+ monitors | Split into multiple pinger instances (shard by userId) or move to a queue (Cloud Tasks / BullMQ) |
Cost drivers (Blaze, pay-as-you-go):
- Writes: 1 batched write per ping →
$0.18 / 100Kwrites - Reads / storage: cheap (
$0.06 / 100Kreads,$0.18 / GB / month) - To cut costs: raise intervals (5 min is 5× cheaper than 1 min).
The frontend scales automatically on Vercel — every user query is indexed per userId.
| Symptom | Cause / Fix |
|---|---|
service-account.json not found |
Add the file locally or set SERVICE_ACCOUNT_JSON on Render |
The query requires an index |
Shouldn't happen anymore — queries filter by equality only. If you see it, you're on an old build: redeploy |
Missing or insufficient permissions |
Firestore rules not published, or an old build querying without the userId filter — redeploy the frontend |
Hydration failed |
Old dev server cached pre-env state — restart npm run dev |
| Pings every second (not interval) | Old backend build (scheduler loop bug, fixed) — redeploy |
tee is not recognized |
Old npm script on Windows — the dev script is now next dev -p 3000 |
| Backend on free Render instance | Free instances sleep → pings stop. Use a paid Background Worker |
| Google sign-in fails after deploy | Add your domain to Firebase Authorized domains, and https://yourdomain/__/auth/handler to the OAuth client redirect URIs |
405 status on monitors |
Any response counts as success now; if you still see failures, the request isn't reaching the server (network-level error) |
The frontend ships with production-grade SEO out of the box:
- Keyword-targeted metadata (title/description/keywords) for "render ping", "render uptime", "render keep alive" and related queries
- JSON-LD structured data:
SoftwareApplication,Person,Organization,WebSite,WebPage,FAQPage,BreadcrumbList - Dynamic OpenGraph image (
/opengraph-image) and Twitter card (/twitter-image) - Auto-generated
sitemap.xml,robots.txt, and PWAmanifest.webmanifest - Author attribution (Shyamnath Sankar) in metadata, schemas, and footer
RenderPing is free forever — no pricing tiers, no credit card. If it keeps your Render apps alive, support the project:
- ☕ Buy Shyamnath Sankar a coffee
- 🐛 Found a bug? Use the Report a bug button in the dashboard (GitHub issues)
- ⭐ Star and share on GitHub
MIT. Built for the Render dev community. Not affiliated with Render Inc.