{"slug": "i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple", "title": "I tried to add a simple hit counter to my app and ended up learning why \"simple\" free APIs quietly fail on mobile", "summary": "A developer building Daily Doodle, a static GitHub Pages app, found that a free public hit-counter API (countapi.mileshilliard.com) worked on desktop but silently failed on mobile because the domain was blocked at the network level by ad blockers, carrier filtering, or DNS blocklists. After a fetch-plus-image-pixel fallback also failed, the developer switched to Firebase Realtime Database, using its server-side atomic increment ('.sv': {'increment': 1}) to avoid race conditions, since firebaseio.com is rarely blocklisted.", "body_md": "Social proof is good when you have a new app. I wanted people to know that others were using the app. I literally asked Claude for a 90s style website counter. I used the example of the famous burger sign that says, \"billions served.\"\n\n*Image source: [The Flintstones Wiki on Fandom](https://fandom.com)*\n\nside note- this movie was so magical when I was a little kid!\n\nTurned out to be a good little rabbit hole, so I figured it was worth writing\n\nup on its own — not just \"here's a counter,\" but what actually broke along\n\nthe way and why.\n\n🔗 See it in action: [https://theplaidscientist.github.io/dailydoodle/](https://theplaidscientist.github.io/dailydoodle/)\n\n💻 Code: [https://github.com/theplaidscientist/dailydoodle](https://github.com/theplaidscientist/dailydoodle)\n\n[Daily Doodle](https://dev.to/plaidscientist/i-vibe-coded-my-first-app-ever)\n\nis a static site on GitHub Pages — no backend, no server I control. So the\n\nfirst move was a free public counter service\n\n([countapi.mileshilliard.com](https://countapi.mileshilliard.com/)) — no\n\naccount, no API key, just a `GET` request that increments a number tied to a\n\nkey I made up:\n\n```\nfetch(`https://countapi.mileshilliard.com/api/v1/hit/${COUNTER_KEY}`)\n  .then(r => r.json())\n  .then(data => { counterEl.textContent = data.value; });\n```\n\nWorked immediately on desktop. Yay! It's working! This is gonna be so cool.\n\nI switched to my phone before sending the link to my friend and realized it was still at triple ---. No matter what I did, I couldn't get the counter to update..\n\nThe counter would just... not move on mobile. No error the user would ever\n\nsee, because I'd deliberately built it to fail silently (dashes on screen\n\ninstead of a broken-looking blank) rather than break the actual app if the\n\ncounter service ever had a bad day.\n\nFirst fix I tried: fire the request two ways at once — the normal `fetch()`\n\ncall, plus a fallback using an `<img>` tag pointed at the same endpoint,\n\nsince some ad blockers treat image requests differently than fetch/XHR calls:\n\n``` js\nconst pixel = new Image();\npixel.src = `https://countapi.mileshilliard.com/api/v1/hit/${COUNTER_KEY}?_=${Date.now()}`;\n```\n\nDidn't help. Which was actually useful information — if both request types\n\nfail identically, that's not a request-type problem, that's the whole\n\n*domain* being blocked at the network level (an ad blocker, a mobile\n\ncarrier's filtering, a DNS-level blocklist like NextDNS/AdGuard). Generic\n\ncounter/analytics-sounding domains get swept up in filter lists a lot more\n\nthan people realize.\n\nThe fix wasn't cleverer code — it was picking a backend domain that's\n\nessentially never blocklisted, because too much of the internet depends on\n\nit. Firebase fit: `firebaseio.com` is Google infrastructure that a huge\n\nnumber of mainstream apps rely on, so blocklists generally leave it alone.\n\nSetup, for anyone who wants to do this on their own static site:\n\n```\n// Read the current count\nfetch(`${DB_URL}/counters/dailyDoodle.json`)\n  .then(r => r.json())\n  .then(value => { counterEl.textContent = value || 0; });\n\n// Increment it atomically (safe even if two people spin at once)\nfetch(`${DB_URL}/counters/dailyDoodle.json`, {\n  method: 'PUT',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ '.sv': { 'increment': 1 } })\n})\n  .then(r => r.json())\n  .then(value => { counterEl.textContent = value; });\n```\n\nThat `.sv: { increment: 1 }` bit is Firebase's server-side increment — the\n\nmath happens on their server, not in the browser, so there's no race\n\ncondition if two people hit spin at the same moment.\n\nTest mode means the database is publicly writable by anyone who finds the\n\nURL — genuinely fine for a number nobody can really abuse in a meaningful\n\nway, but worth knowing if you're reusing this pattern for anything with\n\nactual sensitive data. \n\nHave you had this problem? Did you solve it similarly or differently?", "url": "https://wpnews.pro/news/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple", "canonical_source": "https://dev.to/plaidscientist/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple-free-apis-quietly-1lnd", "published_at": "2026-09-16 14:09:00+00:00", "updated_at": "2026-09-16 14:43:39.231714+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Daily Doodle", "GitHub Pages", "countapi.mileshilliard.com", "Firebase", "Google", "Claude"], "alternates": {"html": "https://wpnews.pro/news/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple", "markdown": "https://wpnews.pro/news/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple.md", "text": "https://wpnews.pro/news/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple.txt", "jsonld": "https://wpnews.pro/news/i-tried-to-add-a-simple-hit-counter-to-my-app-and-ended-up-learning-why-simple.jsonld"}}