PostHog, or several tools in one place
A typical product tooling set means analytics in one place, session replay in another, feature flags in a third, and surveys in a fourth. Four bills, four scripts on the page, and four data sets you cannot line up against each other.
PostHog combines those into one platform: event analytics, session replay, feature flags, experiments, surveys, error tracking, and a data warehouse. The code is open, so it can be self hosted, though most teams use the managed service.
The biggest benefit lies not in the feature count but in the data being shared. An analytics event, a session recording, and an experiment variant assignment concern the same user, so from a chart showing a conversion drop you reach, with one click, the recording of the person who gave up.
The free tier and the billing model
Start with the money, since that is where the decision most often gets made and where the unpleasant surprise most often appears.
The free tier is unusually broad for this category: roughly a million analytics events a month, a few thousand session recordings, a million feature flag requests, and a pool of error events. There is no time limit and no cap on team members, so a small product fits inside it for years.
Above the tier, billing follows usage with no fixed subscription. The per event rate falls as volume rises, so a large deployment pays noticeably less per unit than a small one.
Price it against your own numbers before rolling it out, since the gap between an estimate and a bill comes from things easy to overlook. I cover them in the next section, because they deserve separate treatment.
Three multipliers that ruin estimates
This is the most practical part of this text and the thing the vendor's material states less plainly than it should.
The first multiplier is automatic event capture. On by default, it records clicks, form submissions, and page views without any code, which is excellent at the start and expensive later. One user interaction can generate several events, and with scroll tracking the count grows in a way nobody planned.
The second is events attributed to an identified user. They cost many times more than anonymous events, since they require extra processing. An application where every user is logged in therefore pays considerably more than the base rate suggests.
The third is syncing data from external systems into the warehouse, billed per row. Connecting a payment system or a customer support system looks innocent in configuration and can add a line item comparable to everything else.
Those three multiply against each other rather than adding, and that is the source of the stories about bills several times higher than expected. The good news is that all three are controllable.
Spending limits worth setting immediately
The platform lets you set a monthly amount limit separately for each product: analytics, session replay, feature flags, and error tracking. Once a limit is passed, data ingestion for that product stops for the rest of the period.
That is the first thing to do after creating an account, before writing a line of code. A limit set at what you are willing to pay converts the risk of an unexpected bill into the risk of losing some data, a considerably milder problem.
The second safeguard is constraining automatic event capture. It can be disabled entirely, sending only events you define, or narrowed to selected page elements.
import posthog from 'posthog-js'
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: 'https://eu.i.posthog.com',
autocapture: false,
capture_pageview: false,
session_recording: { maskAllInputs: true }
})
posthog.capture('order_placed', {
value: 249.99,
item_count: 3
})Disabling automatic capture and sending a dozen or so named events usually yields better analytics than a thousand events gathered on their own. A named event corresponds to a step in a process you care about, while a click on a random button corresponds to nothing.
The third is sampling session recordings. Recording every session rarely makes sense, since you will watch a dozen or so anyway. Recording a fraction of traffic, or only sessions meeting a condition such as ending in an error, gives the same insight at a fraction of the price.
The fourth, useful at larger scale, is moving some signals to tools that handle them more cheaply. Application crash tracking does well in Sentry, and basic traffic statistics suit any lightweight analytics tool. Keeping here only what benefits from shared data, meaning product events, recordings, and experiments, is sometimes cheaper than sending everything to one place.
Feature flags and experiments
This is where the platform replaces a separate, usually expensive tool, and does it well.
if (posthog.isFeatureEnabled('new-cart')) {
return <NewCart />
}
return <OldCart />A flag lets you ship code to production with a feature disabled, then enable it for a slice of users. That separates deployment from release, one of those process changes that a year later seem obvious.
An experiment is a flag with variant assignment and outcome measurement. You define a metric, the platform splits traffic and computes statistical significance, so you need not do it in a spreadsheet.
Two things deserve knowing before a first experiment. The first is fixing the metric before the start rather than after seeing results, since hunting for a metric that came out favourably is a way to confirm any thesis. The second is duration: an experiment stopped after two days because a variant was winning usually measures novelty rather than value.
Flag requests are billed too, so querying them inside a render loop is a cost removable with one call per session. On the server you read flags through an asynchronous call with an explicit user identifier.
import { PostHog } from 'posthog-node'
const posthog = new PostHog(process.env.POSTHOG_KEY!, {
host: 'https://eu.i.posthog.com',
flushAt: 20,
flushInterval: 10_000
})
const enabled = await posthog.isFeatureEnabled('new-cart', user.id)Two constructor settings drive both the bill and performance. The first says after how many events to send a batch, the second after how long to send it despite an incomplete batch. The defaults are safe, but in a short lived process, a serverless function for instance, you have to flush explicitly before exit or the events are lost.
await posthog.shutdown()Plan for what happens when a flag state response never arrives, too. An ad blocker, a slow network, or a service outage leaves the value unknown, and code assuming a default of enabled will then show some users a feature they were not meant to see. Defaulting to disabled and rendering the old variant until an answer arrives is the safer setting.
const state = posthog.isFeatureEnabled('new-cart')
if (state === undefined) return <OldCart />
return state ? <NewCart /> : <OldCart />Distinguishing unknown from disabled matters here, because the check returns two different things in those cases, while the shorthand treats both alike.
A separate good habit is cleaning up flags after a release completes. A condition that has returned the same value for six months stays in the code as a dead branch, and with twenty such conditions nobody knows which paths remain reachable. Removing a flag together with its unused branch belongs to the release task rather than to a someday list.
PostHog against the alternatives
| Option | Strength | Weakness | Pick it when |
|---|---|---|---|
| PostHog | Several tools over shared data, broad free tier | Bills jump under careless configuration | A product team wanting one place |
| Traffic analytics | Simplicity, usually free | No replay, flags, or experiments | An informational site without logins |
| Sentry | Deep error diagnostics | Product analytics in its infancy | Emphasis on quality and errors |
| Self hosting | Data stays with you, no per event fees | Upkeep, scaling, and backups are yours | Data residency requirements |
The decision depends on how many of these features you genuinely use. A team using only page views and basic events overpays for capabilities it never touches. A team running experiments, watching recordings, and releasing features to a slice of traffic saves on three separate bills at once.
The second row means in practice a tool along the lines of Plausible: a cookieless script, so the site needs no consent banner, one dashboard instead of dozens of reports, and open source code you can host yourself. There are no session recordings, feature flags, or experiments there, so it suits a site that wants its visit count rather than one studying the behaviour of logged in users.
The last row deserves honest treatment. Self hosting removes per event fees and introduces the cost of maintaining a cluster processing large data volumes, which under serious traffic is sometimes more expensive than the managed service. That choice is usually driven by legal requirements rather than savings.
Setup and the data model
Installation is a script on the page or a package in the project, and with a Next.js application two things deserve attention.
The first is initialising on the client side only, since the library reaches for browser objects. Calling it during server rendering ends in an error, and the answer is placing it in a client component that runs after mounting.
The second is tracking route changes. A single page application does not reload on navigation, so automatic page view counting sees only the first entry. You must send the event manually on path change, and that is the most common cause of understated statistics in such applications.
Consider sending events from the server rather than the browser too. Events covering payments, plan changes, or process completion are then immune to ad blockers and to a tab closing halfway, and those are usually the events carrying the most business meaning.
The data model rests on events with properties and on persons those events attach to. Plan the properties from the start, since adding them later does not change events already recorded. An order event without a basket value looks innocent in the first week and makes revenue uncountable for a whole quarter backwards.
Settle event naming once as well. A convention using a past tense verb, written in lowercase with underscores, is arbitrary and effective, whereas mixing three conventions in one project produces an event list nobody can search six months later.
Privacy and compliance
The platform offers a choice of data residency region, including a European one, which under personal data requirements is sometimes a condition of entry.
Session recordings need separate attention, since they capture what the user sees. Masking form fields is a setting worth enabling from the start, with explicit exclusions added for fields holding sensitive data. A card number visible in a recording is a problem that cannot be undone after the fact.
Remember user consent too. A script gathering events and recordings falls under the same rules as other analytics tools, so starting it before consent is a risk easily avoided by initialising the library only after the user decides.
Common mistakes
The first is no spending limit. Setting one takes a minute and converts the risk of an unexpected bill into the risk of missing some data.
The second is leaving automatic event capture unreviewed. That is the main cause of bills several times higher than the estimate.
The third is identifying every user without need. Attributed events cost many times more than anonymous ones.
The fourth is recording every session. You will watch a dozen or so while paying for all of them, whereas sampling gives the same insight for less.
The fifth is fixing an experiment's metric after seeing results. That is a way to confirm any thesis rather than to learn anything.
The sixth is recordings without form field masking. Sensitive data captured in a recording is a problem that cannot be fixed retroactively.
FAQ
Is PostHog free?
Up to a point, and that point is broad: roughly a million analytics events a month, a few thousand session recordings, and a million feature flag requests, with no time limit and no cap on team members. Above the tier you pay for usage with no fixed subscription.
Why do bills come out higher than estimates?
Usually because of three things multiplying against each other: automatic capture generating several events per interaction, events attributed to an identified user costing many times more than anonymous ones, and syncing data from external systems billed per row.
Will it replace a separate feature flag tool?
Yes, and that is one of the stronger arguments for this platform, since separate tools of that class run expensive. Flags work alongside experiments and analytics over shared data, so a test result appears in the same place as everything else.
Can I self host it?
Yes, the code is open. Do price the upkeep of a cluster processing large data volumes, though, since under serious traffic it can exceed the managed service bill. That choice is usually driven by data residency requirements.
Are session recordings safe for personal data?
Only under the right configuration. Masking form fields is worth enabling from the start, with sensitive fields excluded explicitly. The library itself should start only after obtaining user consent, as with other analytics tools.
Documentation sits on the project site, and the code in the GitHub repository.