Overview
Even the best implementations run into issues. This guide covers the most common PostHog problems, how to diagnose them, and how to fix them quickly. Whether events aren't showing up, session recordings aren't capturing, or feature flags aren't working as expected, you'll find answers here.
The key to debugging PostHog is understanding the data flow: SDK → ingestion endpoint → processing → storage → dashboard. Problems can occur at any stage, and knowing where to look saves time.
Events Not Showing Up
This is the most common issue. Events are being tracked in code but don't appear in PostHog.
Diagnosis
1. Check browser console:
// Enable debug mode
posthog.init('YOUR_API_KEY', {
api_host: 'https://app.posthog.com',
debug: true // Verbose logging
});
// You should see:
// [PostHog] Event captured: your_event_name
// [PostHog] Sending batch of N events
2. Check network requests:
- Open browser DevTools → Network tab
- Filter for
posthogor your self-hosted domain - Look for POST requests to
/e/or/batch/ - Inspect request payload and response
Expected successful response:
{
"status": 1
}
3. Check PostHog activity:
- Go to Activity in PostHog dashboard
- Filter by your event name or distinct_id
- Events should appear within 10-30 seconds
Common Causes & Solutions
API key incorrect:
// Wrong
posthog.init('ph_test_WRONG_KEY', { ... });
// Correct - get from Settings > Project
posthog.init('phc_YOUR_ACTUAL_PROJECT_API_KEY', { ... });
Wrong API host:
// Wrong for Cloud users
posthog.init('YOUR_API_KEY', {
api_host: 'https://posthog.com' // This is the marketing site!
});
// Correct for Cloud
posthog.init('YOUR_API_KEY', {
api_host: 'https://app.posthog.com' // or 'https://eu.posthog.com' for EU
});
// Correct for self-hosted
posthog.init('YOUR_API_KEY', {
api_host: 'https://posthog.yourdomain.com'
});
Ad blockers: Ad blockers often block analytics tools. Test in incognito or with ad blocker disabled.
Solution - Use a reverse proxy:
// Route PostHog through your domain to avoid blockers
posthog.init('YOUR_API_KEY', {
api_host: 'https://yourdomain.com', // Your domain
ui_host: 'https://app.posthog.com' // PostHog dashboard
});
// Set up reverse proxy (Nginx example)
// location /posthog/ {
// proxy_pass https://app.posthog.com/;
// }
Content Security Policy (CSP) blocking:
<!-- Add PostHog domains to CSP -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://app.posthog.com;
connect-src 'self' https://app.posthog.com;">
SDK not initialized before tracking:
// Wrong - tracking before init completes
posthog.init('YOUR_API_KEY');
posthog.capture('event'); // Might be lost
// Correct - wait for init
posthog.init('YOUR_API_KEY', {
loaded: (posthog) => {
posthog.capture('app_loaded');
}
});
CORS issues (self-hosted):
// Self-hosted PostHog needs CORS configured
// In your PostHog config:
// ALLOWED_ORIGIN_HOSTS: ["yourdomain.com"]
Events batched/delayed: PostHog batches events by default (every 10 seconds or 20 events). This is normal.
// Force immediate send for testing
posthog.capture('test_event');
posthog._send_request({}); // Internal method, use cautiously
// Better: reduce batch interval
posthog.init('YOUR_API_KEY', {
loaded: (posthog) => {
posthog.set_config({
batch_size: 1, // Send immediately
batch_interval: 1000 // 1 second
});
}
});
Session Recordings Not Working
Session recordings require proper setup and can fail silently if misconfigured.
Diagnosis
1. Check if recording is enabled:
posthog.init('YOUR_API_KEY', {
api_host: 'https://app.posthog.com',
autocapture: true,
session_recording: {
enabled: true // Must be true
}
});
// Or check programmatically
console.log(posthog.sessionRecordingStarted()); // Should return true
2. Check browser console for errors:
[PostHog] Session recording failed to start
[PostHog] rrweb not loaded
3. Check network requests:
- Look for requests to
/s/(session recording endpoint) - Recordings upload in chunks as user interacts
4. Check PostHog dashboard:
- Go to Session Recordings
- Filter by your distinct_id
- Recordings appear within minutes
Common Causes & Solutions
Recording disabled in project settings:
- Go to PostHog Settings > Project
- Check "Enable session recordings" is ON
- Set minimum duration if needed
Third-party iframes blocking: Session recordings can't capture content inside third-party iframes (e.g., YouTube, Twitter embeds). This is a browser security limitation.
Performance issues:
// Reduce recording quality for performance
posthog.init('YOUR_API_KEY', {
session_recording: {
enabled: true,
quality: 'low', // 'low', 'medium', 'high'
sampling_ratio: 0.5 // Record only 50% of sessions
}
});
Mask sensitive data:
posthog.init('YOUR_API_KEY', {
session_recording: {
enabled: true,
maskAllInputs: true, // Mask all form inputs
maskTextSelector: '.sensitive-data', // Mask specific elements
blockClass: 'ph-no-record', // Don't record these elements
blockSelector: '[data-private]' // Don't record by selector
}
});
Single Page App (SPA) navigation:
// For React, Vue, Angular, etc.
posthog.init('YOUR_API_KEY', {
capture_pageview: true, // Track route changes
session_recording: {
enabled: true,
recordCrossOriginIframes: false // Usually need to be false
}
});
// Manually trigger pageview on route change if needed
router.afterEach((to, from) => {
posthog.capture('$pageview');
});
Canvas/WebGL not captured: PostHog can't record canvas or WebGL content. This is a limitation of session replay technology.
Feature Flags Not Working
Feature flags are powerful but can be tricky if user identification or flag evaluation isn't set up correctly.
Diagnosis
1. Check flag exists and is enabled:
- Go to Feature Flags in PostHog
- Ensure flag is active (not draft)
- Check rollout percentage or filters
2. Check flag evaluation:
// Check if flag is enabled for current user
const isEnabled = posthog.isFeatureEnabled('new-dashboard');
console.log('Flag enabled:', isEnabled);
// Check all flags
console.log('All flags:', posthog.getAllFlags());
// Check feature flag value (for multivariate flags)
const variant = posthog.getFeatureFlag('experiment-checkout');
console.log('Variant:', variant);
3. Enable debug mode:
posthog.init('YOUR_API_KEY', {
api_host: 'https://app.posthog.com',
debug: true, // Logs flag evaluations
advanced_disable_decide: false // Ensure /decide endpoint is called
});
Common Causes & Solutions
Flag not loaded yet:
// Wrong - checking before flags load
posthog.init('YOUR_API_KEY');
if (posthog.isFeatureEnabled('new-dashboard')) {
// Might evaluate incorrectly
}
// Correct - wait for flags to load
posthog.onFeatureFlags(() => {
if (posthog.isFeatureEnabled('new-dashboard')) {
// Now reliable
}
});
// Or use callback
posthog.init('YOUR_API_KEY', {
loaded: (posthog) => {
posthog.onFeatureFlags(() => {
// Flags ready
});
}
});
User not identified:
// Feature flags rely on distinct_id
// Anonymous users get flags, but targeting won't work until identified
// Identify user first
posthog.identify('user_123', {
email: 'user@example.com',
plan: 'pro'
});
// Then check flags
posthog.onFeatureFlags(() => {
const hasFeature = posthog.isFeatureEnabled('pro-only-feature');
});
Flag targeting doesn't match user:
// If flag is targeted to users with plan: "enterprise"
// But user has plan: "pro", flag won't be enabled
// Check user properties
console.log(posthog.persistence.properties());
// Ensure properties match flag targeting
posthog.setPersonProperties({
plan: 'enterprise' // Now flag might be enabled
});
// Force reload flags
posthog.reloadFeatureFlags();
Local evaluation disabled: For high-performance scenarios, enable local flag evaluation:
posthog.init('YOUR_API_KEY', {
api_host: 'https://app.posthog.com',
advanced_disable_feature_flags: false,
advanced_disable_decide: false, // Needed for flag evaluation
persistence: 'localStorage' // or 'cookie'
});
Server-side flags:
// Node.js
const { PostHog } = require('posthog-node');
const posthog = new PostHog('YOUR_API_KEY');
// Check flag server-side
const isEnabled = await posthog.isFeatureEnabled(
'new-api-endpoint',
'user_123'
);
if (isEnabled) {
// Use new endpoint
}
// Remember to shutdown
await posthog.shutdown();
User Identity Issues
Problems with user identification can break analytics, recordings, and feature flags.
Diagnosis
Check current distinct_id:
console.log('Distinct ID:', posthog.get_distinct_id());
console.log('User properties:', posthog.persistence.properties());
Check user timeline in PostHog:
- Go to Persons & Groups > Persons
- Search for your user
- View their event timeline
- Check for duplicate profiles
Common Causes & Solutions
Identifying too late:
// Wrong - user does stuff before being identified
posthog.capture('page_viewed');
posthog.capture('button_clicked');
// ... later ...
posthog.identify('user_123'); // Previous events orphaned
// Correct - identify immediately on login/signup
function handleLogin(userId) {
posthog.identify(userId);
// Now all events are tied to user_123
}
Identifying too often:
// Wrong - identifying on every page load
useEffect(() => {
posthog.identify('user_123'); // Don't do this!
}, []);
// Correct - identify only on login/signup
function handleLogin(userId) {
posthog.identify(userId);
// Store in localStorage or session to avoid re-identifying
}
Not resetting on logout:
// Wrong - same distinct_id persists after logout
function handleLogout() {
// Just log out
}
// Correct - reset PostHog on logout
function handleLogout() {
posthog.reset(); // Clears distinct_id and generates new anonymous ID
// Complete logout...
}
Multiple devices/browsers: PostHog merges user profiles when you identify with the same user ID from different devices. This is expected behavior.
Server-side vs client-side distinct_id mismatch:
// Ensure same distinct_id on client and server
// Client:
posthog.identify('user_123');
// Server (Node.js):
posthog.capture({
distinctId: 'user_123', // Must match client
event: 'subscription_created'
});
Cross-Domain Tracking Issues
Tracking users across multiple domains requires special configuration.
Diagnosis
Check if user has same distinct_id on both domains:
- Visit domain A, check:
posthog.get_distinct_id() - Navigate to domain B, check:
posthog.get_distinct_id() - Should be the same
Check cookies: PostHog stores distinct_id in cookies. Cross-domain requires cookie sharing.
Solution
Enable cross-domain tracking:
// Domain A (main site)
posthog.init('YOUR_API_KEY', {
api_host: 'https://app.posthog.com',
cross_subdomain_cookie: true, // Share cookies across subdomains
persistence: 'cookie'
});
// Domain B (different domain)
posthog.init('YOUR_API_KEY', {
api_host: 'https://app.posthog.com',
cross_subdomain_cookie: true
});
For completely different domains: You need to pass distinct_id via URL parameter:
// Domain A - add distinct_id to link
const link = `https://domainb.com?ph_distinct_id=${posthog.get_distinct_id()}`;
// Domain B - read from URL and identify
const urlParams = new URLSearchParams(window.location.search);
const distinctId = urlParams.get('ph_distinct_id');
if (distinctId) {
posthog.identify(distinctId);
}
Performance Issues
PostHog should have minimal performance impact, but misconfigurations can cause problems.
Diagnosis
Check bundle size:
# PostHog JS SDK is ~30KB gzipped
# If much larger, you might be bundling unnecessarily
npm run build -- --analyze # For webpack bundle analyzer
Check network waterfall:
- PostHog should load asynchronously
- Events should batch, not send individually
- Session recordings upload incrementally
Solutions
Load PostHog asynchronously:
<script>
!function(t,e){...}(window,document); // Async snippet
posthog.init('YOUR_API_KEY');
</script>
Reduce autocapture overhead:
posthog.init('YOUR_API_KEY', {
autocapture: {
css_selector_blacklist: ['.no-track', '#ignore-this'] // Reduce DOM scanning
}
});
Optimize session recordings:
posthog.init('YOUR_API_KEY', {
session_recording: {
quality: 'low', // Smaller payload
sampling_ratio: 0.25, // Record only 25% of sessions
minimumDuration: 5000 // Don't record sessions < 5 seconds
}
});
Batch events efficiently:
posthog.init('YOUR_API_KEY', {
loaded: (posthog) => {
posthog.set_config({
batch_size: 50, // Send in batches of 50 (default: 20)
batch_interval: 20000 // Every 20 seconds (default: 10000)
});
}
});
Data Quality Issues
Sometimes data arrives but looks wrong.
Missing or Incorrect Properties
Check event payload in network tab:
{
"event": "button_clicked",
"properties": {
"button_name": null, // Should have value
"$current_url": "https://example.com/page"
}
}
Fix:
// Ensure properties are set before capture
const buttonName = button.getAttribute('data-name') || 'unknown';
posthog.capture('button_clicked', {
button_name: buttonName // Now has value
});
Timezone Issues
PostHog stores all timestamps in UTC. If times look wrong, check your dashboard timezone:
- Go to Settings > Project Settings
- Set Display Timezone
- All charts now show in your timezone
Duplicate Events
Cause: Multiple SDK initializations or event tracking in effect hooks
Fix:
// React - initialize only once
useEffect(() => {
if (!window.posthogInitialized) {
posthog.init('YOUR_API_KEY');
window.posthogInitialized = true;
}
}, []); // Empty dependency array
// Don't track in render
function Component() {
posthog.capture('rendered'); // Called on every render
useEffect(() => {
posthog.capture('mounted'); // Called once on mount
}, []);
}
Self-Hosted Issues
Additional issues specific to self-hosted PostHog.
Events Not Ingesting
Check PostHog is running:
docker ps # All services should be up
# Check logs
docker logs posthog-web
docker logs posthog-worker
docker logs posthog-clickhouse
Check ingestion endpoint:
curl -X POST https://posthog.yourdomain.com/capture/ \
-H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"event": "test_event",
"distinct_id": "test_user"
}'
# Should return: {"status":1}
Check ClickHouse:
# Exec into ClickHouse container
docker exec -it posthog-clickhouse clickhouse-client
# Query events table
SELECT event, timestamp FROM events ORDER BY timestamp DESC LIMIT 10;
High Memory Usage
Redis memory:
docker stats posthog-redis
# If high, increase maxmemory in Redis config
ClickHouse memory:
docker stats posthog-clickhouse
# Adjust settings in clickhouse-config.xml
Upgrade Issues
Before upgrading:
# Backup database
docker exec posthog-db pg_dump -U posthog posthog > backup.sql
# Check release notes
# https://github.com/PostHog/posthog/releases
After upgrade:
# Run migrations
docker exec posthog-web python manage.py migrate
Getting Help
Self-Diagnosis Checklist
Before asking for help, check:
- API key is correct
- API host matches your deployment (Cloud vs self-hosted)
- Browser console shows no errors
- Network requests to PostHog succeed (200 responses)
- Events appear in PostHog Activity within 30 seconds
- Ad blockers are disabled or bypassed
- SDK version is up to date
Debug Information to Share
When asking for help, include:
- PostHog SDK version
- Browser/device/OS
- Steps to reproduce
- Console logs (with debug: true)
- Network request/response (from DevTools)
- PostHog project API key (safe to share)
Resources
- Documentation: posthog.com/docs
- Community Slack: posthog.com/slack
- GitHub Issues: github.com/PostHog/posthog
- Support: support@posthog.com (for paid plans)
Enable Support Access
For Cloud users, you can grant PostHog support access to debug issues:
- Go to Settings > Project
- Under Support, toggle "Allow PostHog team to access this project"
- Share project name with support team
This lets PostHog engineers investigate directly, speeding up resolution.
Remember: Most PostHog issues come down to incorrect API keys, wrong API hosts, or ad blockers. Start there, and you'll solve 80% of problems in minutes.