How I Built a Globally Persistent Like System on Serverless — Zero Database Bills
Adowise Intelligence

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.

Mohammad Altaf
Mohammad Altaf
9 min read·May 18, 2026
0

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 (globalThis is container-scoped)
  • Has its own temporary filesystem (/tmp is 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 Redis
  • POST → Increments or decrements the count atomically using INCR/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:

  1. User clicks the heart
  2. UI immediately increments the count and fills the heart red
  3. A background fetch() call syncs with the server
  4. 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:

  1. Check if a count exists in Redis for the given slug
  2. If not, seed it from the blog post's frontmatter likes field
  3. Use redis.incr(key) for likes, redis.decr(key) for unlikes
  4. 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

  1. 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.

  2. Optimistic UI is non-negotiable. Users expect instant feedback. Always update the UI first, then sync with the server in the background.

  3. localStorage is your friend for personal state. Don't waste server resources tracking which user liked which post. Let the browser handle personal preferences.

  4. Atomic operations prevent data corruption. Never do GET → increment in JS → SET. Always use INCR directly. 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.