Plausible - privacy-first analytics
What is Plausible?
Plausible Analytics is a lightweight, open-source analytics platform created as an alternative to Google Analytics with user privacy in mind. Founded in 2019 in Estonia by Uku Taht and Marko Saric, Plausible grew out of the need for an analytics tool that does not require cookies, GDPR consent banners, and does not collect personal data.
Unlike traditional analytics tools that track users across the web using third-party cookies, Plausible uses a unique approach based on hashing IP addresses combined with User Agent. This means every visit is anonymous, and Plausible does not build user profiles or sell data to advertisers.
History and philosophy of Plausible
Plausible was created as a response to growing concerns about internet privacy and increasingly complex regulations like GDPR (Europe), CCPA (California), and LGPD (Brazil). The creators noticed that most website owners only need basic metrics -- how many people visit the site, where they come from, and which pages are popular.
Google Analytics offers thousands of metrics and dimensions, but most of them are not needed by regular content creators or small businesses. Yet it requires accepting a complicated privacy policy, installing cookie banners, and potentially exposes you to GDPR fines of up to 4% of annual revenue.
Plausible follows the "less is more" philosophy -- a simple dashboard with the most important information, without unnecessary noise and without compromising on privacy.
Why Plausible?
1. No cookies = no GDPR banners
Plausible does not use cookies or any form of persistent storage. You do not need to display annoying banners asking for cookie consent. Your site loads faster and looks more professional without those pop-ups.
2. Extremely lightweight script
The Plausible script weighs less than 1KB (gzipped), while Google Analytics is over 45KB. This means:
- Faster page loading
- Better Core Web Vitals score
- Lower bandwidth usage for visitors
- Smaller carbon footprint for your site
3. Full regulatory compliance
Plausible is fully compliant with:
- GDPR (Europe)
- CCPA (California)
- PECR (UK)
- LGPD (Brazil)
- PIPEDA (Canada)
Data is stored on servers in the European Union, which is a requirement for many European companies and institutions.
4. Open source and self-hosting
Plausible is fully open source (AGPL license). You can:
- See exactly how the code works
- Run your own instance on your servers
- Have 100% control over data
- Never worry about vendor lock-in
5. Simple, readable dashboard
Instead of hundreds of reports and dimensions, you get one elegant dashboard with the most important information:
- Unique visitors
- Pageviews
- Bounce rate
- Time on page
- Traffic sources
- Popular pages
- Geographic locations
- Devices and browsers
6. Ethical approach to business
Plausible is a bootstrapped company without external investors. This means:
- No pressure to monetize user data
- Transparent business model (you pay for the service, you are not the product)
- Long-term stability without the risk of corporate acquisition
Plausible vs Google Analytics
| Feature | Plausible | Google Analytics 4 |
|---|---|---|
| Cookies | No | Yes (first-party) |
| GDPR banner | Not required | Required |
| Script size | <1KB | 45KB+ |
| Personal data | Does not collect | Collects |
| User profiling | No | Yes |
| Sharing with Google | No | Yes (by default) |
| Open source | Yes (AGPL) | No |
| Self-hosting | Yes | No |
| Price | From $9/mo | "Free"* |
| Complexity | Simple | Very complex |
| Data retention | Indefinite | 2-14 months |
| Real-time | Yes | Limited |
| Data location | EU (Germany) | USA (Google Cloud) |
*Google Analytics is "free", but you pay with your users' data, which Google uses for advertising.
Plausible vs other alternatives
| Feature | Plausible | Fathom | Simple Analytics | Matomo |
|---|---|---|---|---|
| Open source | Yes | No | No | Yes |
| Self-host | Yes | No | No | Yes |
| Price (start) | $9/mo | $14/mo | $9/mo | Free |
| Cookies | No | No | No | Optional |
| JS size | <1KB | 2KB | 3KB | 22KB+ |
| EU hosting | Yes | Yes | Yes | Optional |
| Real-time | Yes | Yes | Yes | Yes |
| Custom events | Yes | Yes | Yes | Yes |
| Funnels | Yes | Yes | Yes | Yes |
| API | Yes | Yes | Yes | Yes |
Installing Plausible
Basic installation (HTML)
Add the script to the <head> section of your page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My website</title>
<!-- Plausible Analytics -->
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js">
</script>
</head>
<body>
<!-- Page content -->
</body>
</html>Next.js App Router
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<Script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js"
strategy="afterInteractive"
/>
</head>
<body>{children}</body>
</html>
)
}Next.js Pages Router
// pages/_app.tsx
import Script from 'next/script'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js"
strategy="afterInteractive"
/>
<Component {...pageProps} />
</>
)
}Remix
In Remix the tag goes into app/root.tsx, because that file renders the whole document including the head. One caveat concerns the name: development of this line moved into React Router 7, so the example below describes version 2, and in a new project you make the same edit in React Router's root file.
// app/root.tsx
import { Links, Meta, Outlet, Scripts } from '@remix-run/react'
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js"
/>
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
)
}Astro
---
// src/layouts/Layout.astro
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>{title}</title>
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js">
</script>
</head>
<body>
<slot />
</body>
</html>Gatsby
// gatsby-ssr.js
import React from 'react'
export const onRenderBody = ({ setHeadComponents }) => {
setHeadComponents([
<script
key="plausible"
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js"
/>
])
}Nuxt.js 3
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
defer: true,
'data-domain': 'yourdomain.com',
src: 'https://plausible.io/js/script.js'
}
]
}
}
})SvelteKit
<!-- src/app.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js">
</script>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>Custom events
Tracking events with JavaScript
declare global {
interface Window {
plausible: (
event: string,
options?: {
props?: Record<string, string | number | boolean>
callback?: () => void
revenue?: { currency: string; amount: number }
}
) => void
}
}
function trackSignup() {
window.plausible('Signup')
}
function trackPurchase(plan: string, price: number) {
window.plausible('Purchase', {
props: {
plan,
price: price.toString()
}
})
}
function trackSale(amount: number, currency: string = 'USD') {
window.plausible('Sale', {
revenue: { currency, amount }
})
}
function trackDownload(fileName: string) {
window.plausible('Download', {
props: { file: fileName },
callback: () => {
console.log('Event tracked successfully')
}
})
}React hook
// hooks/usePlausible.ts
import { useCallback } from 'react'
type PlausibleEvent = {
name: string
props?: Record<string, string | number | boolean>
revenue?: { currency: string; amount: number }
}
export function usePlausible() {
const trackEvent = useCallback(({ name, props, revenue }: PlausibleEvent) => {
if (typeof window !== 'undefined' && window.plausible) {
window.plausible(name, { props, revenue })
}
}, [])
const trackPageview = useCallback((url?: string) => {
if (typeof window !== 'undefined' && window.plausible) {
window.plausible('pageview', {
props: url ? { url } : undefined
})
}
}, [])
return { trackEvent, trackPageview }
}
function PricingCard({ plan, price }: { plan: string; price: number }) {
const { trackEvent } = usePlausible()
const handlePurchase = () => {
trackEvent({
name: 'Purchase',
props: { plan },
revenue: { currency: 'USD', amount: price }
})
}
return (
<button onClick={handlePurchase}>
Buy {plan} plan for ${price}
</button>
)
}Automatic click tracking
<!-- Add the class plausible-event-name=NAME to the element -->
<a
href="/pricing"
class="plausible-event-name=CTA+Click"
>
View pricing
</a>
<!-- With properties -->
<button
class="plausible-event-name=Button+Click plausible-event-variant=primary"
>
Sign up
</button>
<!-- For forms -->
<form
action="/submit"
class="plausible-event-name=Form+Submit"
>
<input type="email" name="email" required />
<button type="submit">Submit</button>
</form>Extended script options
<!-- Automatic outbound link tracking -->
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.outbound-links.js">
</script>
<!-- File download tracking -->
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.file-downloads.js">
</script>
<!-- All extensions -->
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.tagged-events.outbound-links.file-downloads.js">
</script>
<!-- Hash-based routing (SPA) -->
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.hash.js">
</script>
<!-- Manual pageview tracking for SPA -->
<script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.manual.js">
</script>Goals & conversions
Defining goals in the dashboard
In the Plausible dashboard you can define different types of goals:
1. Pageview goals
/pricing- visits to the pricing page/thank-you- thank you page after purchase/signup/complete- registration completion
2. Custom events
Signup- user registrationPurchase- purchaseDownload- file downloadNewsletter- newsletter subscription
3. Funnels (conversion funnels)
Funnel: Purchase process
1. /products →
2. /cart →
3. /checkout →
4. /thank-you
Conversion: 2.3%Revenue tracking
function trackPurchase(orderId: string, amount: number) {
window.plausible('Purchase', {
revenue: {
currency: 'USD',
amount
},
props: {
order_id: orderId
}
})
}
async function completeCheckout(cart: CartItem[]) {
const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
const response = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify(cart)
})
if (response.ok) {
const { orderId } = await response.json()
window.plausible('Purchase', {
revenue: { currency: 'USD', amount: total },
props: {
order_id: orderId,
items_count: cart.length.toString()
}
})
}
}Filtering and segmentation
Available filters in the dashboard
Plausible lets you filter data by:
Traffic sources:
- Source (Google, Facebook, Twitter, etc.)
- Referrer (specific referring page)
- UTM parameters (campaign, source, medium, term, content)
Technology:
- Browser (Chrome, Firefox, Safari, Edge)
- OS (Windows, macOS, iOS, Android, Linux)
- Device type (Desktop, Mobile, Tablet)
- Country / Region / City
Pages:
- Entry page
- Exit page
- Page (visited page)
UTM tracking
<!-- Link with UTM parameters -->
<a href="https://yourdomain.com?utm_source=newsletter&utm_medium=email&utm_campaign=summer_sale">
Check out the promotion
</a>function createUtmLink(
baseUrl: string,
params: {
source: string
medium: string
campaign: string
term?: string
content?: string
}
) {
const url = new URL(baseUrl)
url.searchParams.set('utm_source', params.source)
url.searchParams.set('utm_medium', params.medium)
url.searchParams.set('utm_campaign', params.campaign)
if (params.term) url.searchParams.set('utm_term', params.term)
if (params.content) url.searchParams.set('utm_content', params.content)
return url.toString()
}
const newsletterLink = createUtmLink('https://mystore.com/promo', {
source: 'newsletter',
medium: 'email',
campaign: 'black_friday_2024'
})Plausible API
Access to the stats API is tied to the Business plan and above rather than available at every tier. A limit of six hundred requests an hour applies, which a dashboard querying the API on every page refresh exhausts sooner than it looks. A sensible arrangement caches responses on your side and polls at fixed intervals rather than on user demand.
Stats API
const PLAUSIBLE_API_KEY = process.env.PLAUSIBLE_API_KEY
const SITE_ID = 'yourdomain.com'
interface PlausibleStats {
results: {
visitors: { value: number }
pageviews: { value: number }
bounce_rate: { value: number }
visit_duration: { value: number }
}
}
async function getStats(period: string = '30d'): Promise<PlausibleStats> {
const response = await fetch(
`https://plausible.io/api/v1/stats/aggregate?` +
`site_id=${SITE_ID}&period=${period}&` +
`metrics=visitors,pageviews,bounce_rate,visit_duration`,
{
headers: {
Authorization: `Bearer ${PLAUSIBLE_API_KEY}`
}
}
)
return response.json()
}
interface TopPages {
results: Array<{
page: string
visitors: number
}>
}
async function getTopPages(limit: number = 10): Promise<TopPages> {
const response = await fetch(
`https://plausible.io/api/v1/stats/breakdown?` +
`site_id=${SITE_ID}&period=30d&property=event:page&limit=${limit}`,
{
headers: {
Authorization: `Bearer ${PLAUSIBLE_API_KEY}`
}
}
)
return response.json()
}Real-time API
async function getCurrentVisitors(): Promise<number> {
const response = await fetch(
`https://plausible.io/api/v1/stats/realtime/visitors?site_id=${SITE_ID}`,
{
headers: {
Authorization: `Bearer ${PLAUSIBLE_API_KEY}`
}
}
)
return response.json()
}
function LiveVisitors() {
const [visitors, setVisitors] = useState<number>(0)
useEffect(() => {
const fetchVisitors = async () => {
const count = await getCurrentVisitors()
setVisitors(count)
}
fetchVisitors()
const interval = setInterval(fetchVisitors, 30000)
return () => clearInterval(interval)
}, [])
return (
<div className="flex items-center gap-2">
<span className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
</span>
<span>{visitors} active users</span>
</div>
)
}Events API
async function trackServerEvent(
eventName: string,
props?: Record<string, string>,
userAgent?: string,
ip?: string
) {
await fetch('https://plausible.io/api/event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent || 'Node.js',
'X-Forwarded-For': ip || ''
},
body: JSON.stringify({
name: eventName,
url: `https://${SITE_ID}`,
domain: SITE_ID,
props
})
})
}
export async function POST(request: Request) {
const { action, data } = await request.json()
await trackServerEvent(
'API Action',
{ action, status: 'success' },
request.headers.get('user-agent') || undefined,
request.headers.get('x-forwarded-for') || undefined
)
return Response.json({ success: true })
}Self-hosting Plausible
Docker Compose setup
# docker-compose.yml
version: "3.8"
services:
plausible:
image: plausible/analytics:v2.0
restart: always
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
depends_on:
- plausible_db
- plausible_events_db
ports:
- 8000:8000
env_file:
- plausible-conf.env
plausible_db:
image: postgres:14-alpine
restart: always
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=postgres
plausible_events_db:
image: clickhouse/clickhouse-server:23.3.7.5-alpine
restart: always
volumes:
- event-data:/var/lib/clickhouse
- ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
- ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
db-data:
event-data:Environment configuration
# plausible-conf.env
# Required settings
BASE_URL=https://analytics.yourdomain.com
SECRET_KEY_BASE=random-64-character-string-generate-with-openssl
# Database
DATABASE_URL=postgres://postgres:postgres@plausible_db:5432/plausible_db
CLICKHOUSE_DATABASE_URL=http://plausible_events_db:8123/plausible_events_db
# Email (optional, for notifications)
MAILER_EMAIL=plausible@yourdomain.com
SMTP_HOST_ADDR=smtp.yourdomain.com
SMTP_HOST_PORT=587
SMTP_USER_NAME=plausible@yourdomain.com
SMTP_USER_PWD=smtp-password
SMTP_HOST_SSL_ENABLED=false
SMTP_RETRIES=2
# Optional
DISABLE_REGISTRATION=true
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=Running the instance
# Download configuration files
git clone https://github.com/plausible/community-edition.git plausible
cd plausible
# Generate secret key
openssl rand -base64 64 | tr -d '\n'
# Edit plausible-conf.env with the generated key
# Start the services
docker compose up -d
# Check logs
docker compose logs -f plausible
# Plausible will be available at http://localhost:8000Nginx reverse proxy
# /etc/nginx/sites-available/plausible
server {
listen 80;
server_name analytics.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name analytics.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/analytics.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/analytics.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /js/script.js {
proxy_pass http://127.0.0.1:8000/js/script.js;
proxy_set_header Host $host;
proxy_cache_valid 200 1h;
add_header Cache-Control "public, max-age=3600";
}
}Proxying the script through your own server
To avoid being blocked by ad blockers:
// app/api/script.js/route.ts (Next.js)
export async function GET() {
const response = await fetch('https://plausible.io/js/script.js')
const script = await response.text()
const modifiedScript = script.replace(
'https://plausible.io/api/event',
'https://yourdomain.com/api/event'
)
return new Response(modifiedScript, {
headers: {
'Content-Type': 'application/javascript',
'Cache-Control': 'public, max-age=3600'
}
})
}
// app/api/event/route.ts
export async function POST(request: Request) {
const body = await request.text()
await fetch('https://plausible.io/api/event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': request.headers.get('user-agent') || '',
'X-Forwarded-For': request.headers.get('x-forwarded-for') || ''
},
body
})
return new Response('ok')
}Framework integrations
next-plausible
npm install next-plausible// app/layout.tsx
import PlausibleProvider from 'next-plausible'
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<PlausibleProvider
domain="yourdomain.com"
trackOutboundLinks
trackFileDownloads
taggedEvents
/>
</head>
<body>{children}</body>
</html>
)
}
'use client'
import { usePlausible } from 'next-plausible'
function MyComponent() {
const plausible = usePlausible()
const handleClick = () => {
plausible('Button Click', {
props: { location: 'header', variant: 'primary' }
})
}
return <button onClick={handleClick}>Click me</button>
}@plausible/tracker
npm install @plausible/tracker// lib/plausible.ts
import Plausible from '@plausible/tracker'
export const plausible = Plausible({
domain: 'yourdomain.com',
trackLocalhost: true,
apiHost: 'https://plausible.io'
})
import { plausible } from '@/lib/plausible'
plausible.trackEvent('Signup', {
props: { plan: 'pro' }
})
plausible.enableAutoPageviews()
plausible.enableAutoOutboundTracking()Vue.js plugin
// plugins/plausible.ts
import { createPlausible } from 'vue-plausible'
export default defineNuxtPlugin((nuxtApp) => {
const plausible = createPlausible({
domain: 'yourdomain.com',
trackLocalhost: false
})
nuxtApp.vueApp.use(plausible)
})
<script setup>
import { usePlausible } from 'vue-plausible'
const { trackEvent } = usePlausible()
function handleClick() {
trackEvent('Button Click')
}
</script>Pricing
Billing follows monthly pageviews, and the plans differ in feature scope at the same traffic threshold. That distinction gets muddled, because many roundups quote a single price as though only one plan existed.
| Plan | From | What it adds |
|---|---|---|
| Starter | 9 USD per month | core reports, up to 10k pageviews |
| Growth | 14 USD per month | more team members, shared views |
| Business | 19 USD per month | funnels, revenue data, API access |
| Enterprise | custom quote | above 10M pageviews per month |
The price rises with the pageview threshold, and paying annually gives two months free. The trial runs thirty days and needs no payment card.
One thing is worth noticing when comparing with competitors. Billing by pageviews rather than sessions or users means the cost rises with bot traffic unless it is filtered out. For a site attracting many automated visits, check actual consumption before picking a threshold.
The self hosted edition
The community edition is free under AGPL 3.0 with no pageview cap. You pay only for the server.
Here lies the most common misconception about this product, though, so it needs stating plainly: the free edition is not equivalent to the cloud. Funnels, Google Analytics data import, and team single sign on are available in the paid service only. If any of those matter to you, self hosting will not substitute for them.
Hardware requirements are modest: two gigabytes of memory and twenty gigabytes of disk suffice to start, growing with event history. Running it needs Docker and a domain with a certificate.
Cost it honestly, though. A server at a few dollars a month plus time for upgrades and backups rarely comes out cheaper than nine dollars for the entry plan. Self hosting pays off at high traffic or under a requirement that data physically never leave your infrastructure.
FAQ - frequently asked questions
Does Plausible really not use cookies?
Yes, Plausible does not use any cookies or localStorage. Unique visit identification relies on hashing: IP + User Agent + domain + date. The hash is regenerated every day, so there is no way to track a user across days.
Do I need to display a GDPR banner?
No, if you are only using Plausible. Since Plausible does not collect personal data and does not use cookies, it does not require user consent under GDPR. However, if you use other tools that require consent, you will still need the banner.
How does Plausible handle ad blockers?
The default Plausible script is blocked by some ad blockers. Solutions:
- Proxy the script through your own server
- Use a custom subdomain (analytics.yourdomain.com)
- Self-host the entire instance
Can I use Plausible on multiple domains?
Yes, each domain is a separate "site" in the dashboard. You can also aggregate data from multiple subdomains on a single dashboard.
How long does Plausible store data?
Plausible Cloud stores data indefinitely -- there is no retention limit. In the self-hosted version, you can configure your own retention policies.
Does Plausible track subdomains?
You can configure Plausible to track:
- Only the main domain
- All subdomains together
- Each subdomain separately
<!-- All subdomains together -->
<script data-domain="yourdomain.com" src="..."></script>
<!-- Specific subdomain -->
<script data-domain="blog.yourdomain.com" src="..."></script>How to migrate from Google Analytics?
- Add the Plausible script alongside GA
- Compare data for 2-4 weeks
- Define goals matching your GA events
- Remove the GA script after verification
Plausible also offers historical data import from Google Analytics.
Can I use Plausible to track mobile apps?
This tool is designed primarily for websites. For mobile apps, something oriented around product events makes more sense, PostHog for instance. Events can be sent from an application through the API, but the interface itself is built around web traffic.
How does the free edition differ from the cloud?
The community edition is not equivalent to the paid service. It lacks funnels, Google Analytics data import, and team single sign on. This is the most common misconception about the product.
What this analytics will not tell you
This section matters more than a feature list, because it decides whether the tool fits your questions at all.
You will not learn what a particular user did, unlike tools collecting product events, or Sentry on the error side. The absence of a persistent identifier means no path for one person across several days. If your question is "why did this customer abandon their cart", you need a product analytics tool, not this category.
Nor will you see retention or cohorts in the sense product analytics means. You can check how many visitors returned, but you cannot follow how a group that signed up in a given week behaves afterwards.
The third thing is visitor count accuracy, which matters when reconciling with other tools. Recognition based on a hash of address and browser, refreshed daily, means the same person visiting on Monday and Tuesday counts twice. Compared against Google Analytics that difference is often mistaken for a bug, when it follows directly from declining to tag people persistently.
The fourth is blocking. Although the script is light and unobtrusive, some ad blockers stop it anyway, so numbers will be understated. The scale depends on who visits you: on a technical site losses run into double digits, on a mainstream site they are usually marginal. The remedy is proxying the script through your own domain, which does require configuration on the Next.js side or at the network layer.
The fifth, worth knowing when deciding: there are no session recordings and no click maps. That is a deliberate choice following from the privacy stance rather than a gap awaiting a future release.
The source and releases live in the project repository, and current plans on the pricing page.