
How I Built a Globally Persistent Like System on Serverless — Zero Database Bills
A deep technical walkthrough on implementing real-time, worldwide social engagement using Upstash Redis, Next.js Server Actions, and client-side optimistic rendering.

Every blog, every product page, every portfolio eventually needs one thing: social proof.
The humble "Like" button is the single most powerful micro-interaction on the modern web. It validates content, drives engagement, and creates return visitors. But implementing a like system that works globally — meaning a person in Tokyo likes a post and a reader in London sees the updated count in real-time — is far harder than it sounds.
Most developers reach for heavyweight solutions: spinning up a PostgreSQL instance, deploying a Redis cluster, or paying for Firebase. But what if you could build a production-grade, globally persistent like system with zero database hosting bills and under 100 lines of code?
That's exactly what we shipped at Adowise. Here's the full architectural breakdown.
The Problem: Serverless Containers Are Isolated
If you're running on Vercel, Netlify, or any serverless platform, your backend code runs inside ephemeral containers. Each container:
- Has its own memory (
globalThisis container-scoped) - Has its own temporary filesystem (
/tmpis wiped on cold starts) - May be duplicated across multiple regions simultaneously
This means if User A hits Container 1 and likes a post, User B hitting Container 2 will never see that like. The data is trapped in an isolated memory silo.
Traditional solutions require spinning up a managed database — but that introduces latency, cost, and operational complexity that doesn't match the simplicity of the feature.
The Solution: Upstash Redis over REST
We chose Upstash Redis for one critical reason: it exposes Redis commands over a stateless REST API. This means:
- No persistent TCP connections required (perfect for serverless)
- No driver compatibility issues across edge runtimes
- Free tier covers 10,000 commands/day (more than enough for a growing blog)
- Data is globally persistent and shared across every container invocation
The entire server-side implementation is a single API route file.
Architecture Overview
Our like system has three layers:
1. Client Component (LikeButton)
- Renders a heart icon with optimistic count updates
- Stores personal like state in
localStorage(so the user's heart stays red on revisit) - Calls the server API on every click to sync globally
2. Server API Route (/api/likes)
GET→ Fetches the current global like count from RedisPOST→ Increments or decrements the count atomically usingINCR/DECR- Auto-seeds from frontmatter values on first access (so existing posts start with their editorial counts)
3. Upstash Redis (Persistent Store)
- Each post's likes are stored under a namespaced key:
adowise:blog:likes:{slug} - Atomic operations ensure no race conditions under high concurrency
- Data persists indefinitely across all serverless container lifecycles
The Client-Side Magic: Optimistic Rendering
Nobody wants to click a heart and wait 800ms for a server round-trip before seeing the count change. We use optimistic rendering:
- User clicks the heart
- UI immediately increments the count and fills the heart red
- A background
fetch()call syncs with the server - If the server responds with a corrected count, we silently update
This creates a feeling of instant responsiveness while maintaining global accuracy. The user's personal like state is stored in localStorage under adowise_liked_posts, so even if they close the browser and return days later, their heart stays filled.
Server-Side: Atomic Redis Operations
The server API is beautifully simple. For a like action:
- Check if a count exists in Redis for the given slug
- If not, seed it from the blog post's frontmatter
likesfield - Use
redis.incr(key)for likes,redis.decr(key)for unlikes - Return the new global count
The INCR and DECR commands in Redis are atomic — meaning even if 50 users click "like" simultaneously across different serverless containers worldwide, every single increment is guaranteed to be counted. No race conditions, no lost writes.
Why Not Use a Traditional Database?
We evaluated PostgreSQL, MongoDB, and Firebase before choosing Upstash Redis. Here's why Redis won:
| Factor | PostgreSQL | MongoDB | Firebase | Upstash Redis | |--------|-----------|---------|----------|--------------| | Cold start latency | ~200ms | ~150ms | ~100ms | ~5ms | | Serverless compatible | Needs pooler | Needs Atlas | Yes | Native REST | | Free tier | Limited | 512MB | Spark plan | 10K cmds/day | | Atomic counters | Manual TX | Manual | Increment | Built-in INCR | | Setup complexity | High | Medium | Medium | 2 env vars |
For a simple counter system, Redis is the perfect tool. It was literally designed for this exact use case.
The Result
After deploying this system:
- Every like is globally visible within milliseconds
- Zero database hosting costs (Upstash free tier)
- Zero cold start penalties (REST API, no connection pooling)
- Personal state persists across browser sessions via localStorage
- Works on every page — blog listings and individual post pages
The entire system is three files: one client component, one API route, and two environment variables. No migrations, no schemas, no connection strings, no Docker containers.
Lessons Learned
-
Serverless doesn't mean stateless. You just need to pick the right persistence layer. Upstash Redis over REST is the perfect match for serverless architectures.
-
Optimistic UI is non-negotiable. Users expect instant feedback. Always update the UI first, then sync with the server in the background.
-
localStorage is your friend for personal state. Don't waste server resources tracking which user liked which post. Let the browser handle personal preferences.
-
Atomic operations prevent data corruption. Never do
GET → increment in JS → SET. Always useINCRdirectly. This is the difference between a toy demo and production-grade infrastructure.
The like button is live on every Adowise blog post right now. Try it — click the heart, open the same post in a different browser, and watch the count update globally. That's the power of serverless Redis.

