architectureredisnginxperformancebio-link

How We Serve Thousands of Bio-Link Page Views on a Single VPS (No Microservices Needed)

Discover how Trakk.id handles massive traffic spikes on a single VPS using a split read/write architecture with Redis and Nginx.

Trakk TeamJuly 16, 20264 min read
How We Serve Thousands of Bio-Link Page Views on a Single VPS (No Microservices Needed)

How We Serve Thousands of Bio-Link Page Views on a Single VPS (No Microservices Needed)

Bio-link pages (the "link in bio" mini-sites creators use) look simple on the surface — a static page with a few buttons. But they have a nasty traffic pattern: everything is fine, and then one creator posts a link on social media and your server gets hit with thousands of requests in a few minutes.

At Trakk.id, we run this feature (trakk.id/l/:slug) on a single VPS — no Kubernetes cluster, no fleet of microservices. Here's the architecture that makes that possible, and the two problems we had to solve to get there.

The problem: two competing loads on every request

Every page view actually creates two jobs for your backend:

  • Read load — serve the HTML and static assets (profile photo, background) as fast as possible
  • Write load — log the impression for the creator's analytics dashboard

The naive approach — render the page on every request and write the impression straight to Postgres — works fine at low traffic and falls over exactly when you need it most: during a spike. Every request wakes up your app server, hits the database, and you end up with lock contention and rising latency right as a link goes viral.

The fix was splitting the read path from the write path entirely.

ℹ️ Architecture Principle

By separating the high-frequency read path from the intensive write path, we ensure that a spike in traffic doesn't crash the database or slow down page delivery for users.

Serving: don't let requests touch the app server at all

Instead of rendering bio-link pages dynamically on every request, we pre-render static HTML whenever a creator updates their page from the dashboard. Public traffic never touches our Node.js/Fastify backend — Nginx serves the cached HTML directly at the edge.

Two consequences of this:

  1. vCPU usage stays flat during spikes, because there's no per-request rendering work happening.
  2. User-uploaded assets (avatars, custom themes) live in S3-compatible object storage (we use IDCloudHost for data residency reasons), not on local disk — so large file I/O never competes with the app server either.

The API layer is the only thing that ever touches storage credentials; the public-facing serving path has zero access to them.

< 50ms
Response Time
Sub-50ms response times even during viral traffic spikes

Analytics: stop writing to Postgres on every page view

This was the more interesting problem. Impression tracking needs to be real-time-ish for the dashboard, but you can't afford a database write per visit when a page might get 50,000 views in five minutes.

Our solution: in-memory counting + batched flushes.

page view → Redis INCR biolink:impression:{slug} (< 1ms, no DB touch) every 5 minutes: cron worker → reads all counters from Redis → batch writes them to Postgres in one transaction → resets Redis counters to zero

The key insight: INCR on a Redis key is O(1) and doesn't care whether it's the 1st or 50,000th increment in that window. Postgres only sees one write per slug per 5-minute window, regardless of how many people actually visited. That's what keeps latency stable under load without needing to scale the database vertically.

A quick note on security at the edge

Since these pages are rendered and served publicly, we set X-Frame-Options: SAMEORIGIN on every response. It lets our own dashboard preview a creator's page in an iframe, but blocks any third-party site from embedding it — closing off a common clickjacking vector where an attacker overlays a disguised, clickable link on top of an iframed page.

Takeaway

You don't need a microservices cluster to hit enterprise-grade performance. What you need is to correctly identify which parts of your workload are read-heavy vs. write-heavy, and give each one an architecture suited to it:

  • Read path → pre-render + edge caching, so the app server barely wakes up
  • Write path → in-memory counters + batch flush, so the database sees a fraction of the actual traffic

With that split, a single well-configured VPS running Nginx + Node.js + Redis + Postgres held sub-50ms response times through traffic spikes that would have brought a naive setup down.


We wrote up the full architecture — including the S3 asset isolation setup and more detail on the caching layer — in a technical whitepaper, available here, if you want to go deeper.

Questions or pushback on any of these decisions? I'd love to hear how others have solved similar read/write splitting problems — reach out to our team.

This article is also available in Bahasa Indonesia