PostHog and Upstash Redis on a Multi-Role Next.js App
Page view analytics didn't show where 40% of students were dropping out of enrollment. Custom PostHog funnel events did. Upstash Redis handled edge rate limiting that standard Redis couldn't โ no persistent TCP connection needed. What each tool actually delivered.
I added PostHog because enrollment drop-offs were happening and nobody could tell where. Page view analytics from Vercel told me traffic was reaching the enrollment flow. It did not tell me where users were leaving.
The app is a multi-role portal: students, admins, registrars, finance staff, faculty. About 200 to 300 daily active users depending on the semester. Small enough that A/B testing makes no sense. Big enough that you want to understand what is actually happening.
What PostHog Actually Gave Me
Autocapture was the first thing I turned off. It logs every click, every input focus, every page transition. At 300 DAU across five roles, that is a lot of noise. The signal I needed was specific: where in the enrollment form were students abandoning?
Custom events on form steps were what mattered. I instrumented each step of the multi-step enrollment form with a named event: enrollment_step_started, enrollment_step_completed, enrollment_abandoned. That gave me a real funnel.
The finding was blunt. About 40% of students who reached the document upload step never completed it. Not 40% of all enrollees โ 40% of users who specifically got to that step. The step before it had normal completion rates. The step after rarely got visitors.
Session recordings explained why. Students were confused by the file format requirements. The upload component showed a generic error for wrong file types but did not tell you what file types were actually accepted. Several recordings showed the same pattern: upload attempt, error, stare at the screen, close the tab.
That is the kind of thing you would never find by looking at error logs. The error was being logged. But without the session context you would not know it was a UX problem versus a technical one.
Privacy was a real consideration. These are student records. I configured PostHog to mask all form input fields before enabling session recordings. The recordings capture mouse movement, clicks, and scroll โ not what students typed into name or ID fields. I also added the recording opt-out to the privacy policy update we sent users before enabling it.
What I turned off: feature flags (the app does not need controlled rollouts at this scale) and A/B testing (300 DAU split across five roles is too thin to get meaningful results from split testing). Both features are there if the app grows. For now they add configuration overhead without payoff.
Why Upstash Redis Instead of Self-Hosted
The rate limiting need was straightforward: auth endpoints should not be hammerable. The enrollment submission endpoint should not accept 100 requests a minute from the same IP.
I initially considered a self-hosted Redis instance. The app runs on Vercel, though. Vercel edge middleware does not support persistent TCP connections. Standard Redis clients do not work in edge runtime because they expect to hold a socket open.
Upstash uses an HTTP-based API. Every operation is an HTTPS request to their endpoint. No persistent connection required. It works from edge middleware, serverless functions, and anywhere else an HTTP call works.
For auth endpoints I used a fixed window: 5 login attempts per 15 minutes per IP. Fixed window is simple to reason about and the failure mode is acceptable โ a blocked IP waits out the window. For enrollment operations I used a sliding window: 30 requests per minute. Sliding window is slightly more complex to implement but avoids the burst problem where a user can make 30 requests at 14:59 and 30 more at 15:01 without triggering the fixed window limit.
The atomic increment pattern is what makes rate limiting correct under concurrency. You do not check the count and then increment it โ that creates a race condition. You increment first and check the returned value. If the value after increment exceeds your limit, block the request. Upstash's INCR command with TTL does this in one operation.
I also added a device fingerprint cookie as a third dimension beyond IP and path. IP rotation is cheap. Rotating a device fingerprint requires either clearing cookies or switching browsers, which is more friction for automated abuse and essentially no friction for legitimate users.
Cost: Upstash free tier covered this project entirely. At 200 to 300 DAU and maybe a few thousand Redis operations per day, we were nowhere near the limits. Even at 10x the current scale, the paid tier would be a small line item.
How They Work Together
This part was not planned. It emerged from having both tools running simultaneously.
PostHog session data showed me that peak enrollment activity was between 9am and 11am on weekdays. During those two hours, the enrollment endpoint handles the bulk of its daily load. Setting a rate limit without this data might have blocked legitimate users during peak hours if the threshold was too tight.
PostHog also surfaced bot-like behavior: a cluster of sessions with identical timing patterns, clicking through the enrollment form in under 10 seconds, never completing. Not students. The Upstash rate limiting was already blocking the same IP patterns based on request rate. The PostHog data confirmed that what was being blocked was not real user traffic.
The rate limits I set were calibrated to not interfere with normal peak usage. 30 enrollment requests per minute is higher than any legitimate student would need. The documented abuse patterns from PostHog were in the hundreds per minute.
Tradeoffs Worth Knowing
PostHog's JavaScript bundle adds around 20KB to the client. Not a dealbreaker for this app, but worth knowing if you are optimizing aggressively for initial load time. You can lazy-load it after the page is interactive.
Upstash has cold start latency on the first request of each day. Edge middleware runs the rate limit check on every auth request, so the first request after a quiet period takes slightly longer. I saw this in edge middleware logs โ occasional 150ms spikes on the first request of the morning. Every subsequent request was under 20ms.
I would add both again on similar projects. The operational visibility from PostHog and the protection from Upstash are both cheap relative to the value they provide. The instrumentation took a day to add and tune. The data has been useful every week since.
Found this useful?
Share it with someone who'd appreciate it.
https://wardvisual.com/blogs/posthog-upstash-redis-nextjs

@wardvisual ยท ๐ต๐ญ Dasmarinas City, Cavite PH
Full-stack engineer. Business systems, database optimization, and operations software.