How to Use the PersonHog gRPC Client for Person and Group Data Access in PostHog
Initialize the PersonHog gRPC client using get_personhog_client() after setting PERSONHOG_ADDR in Django settings, check the feature gate with use_personhog(), then call RPC methods like get_person_by_distinct_id() and convert protobuf responses to Django models using helpers in converters.py.
The PostHog platform stores person and group data in a dedicated microservice called PersonHog. Application code accesses this service through a thin, singleton-style gRPC client located in posthog/personhog_client that provides instrumented RPC calls, automatic failover to the Django ORM via feature gates, and utilities to convert protobuf messages into standard Django model instances.
Configuring the PersonHog gRPC Client
The client implementation lives in posthog/personhog_client/client.py. It follows a singleton pattern that lazily creates and caches the client instance on first use.
The PersonHogClient class initializes a gRPC channel with OpenTelemetry interceptors and Prometheus metrics:
# posthog/personhog_client/client.py
class PersonHogClient:
def __init__(
self,
addr: str,
client_name: str = "posthog-django",
timeout_ms: int = 5000,
):
channel = grpc.insecure_channel(addr, options=options)
self._channel = grpc.intercept_channel(
channel,
ClientNameInterceptor(client_name),
MetricsInterceptor(client_name),
)
self._state_monitor = _ChannelStateMonitor(channel, client_name)
self._stub = PersonHogServiceStub(self._channel)
self._timeout = timeout_ms / 1000.0
Access the singleton instance via get_personhog_client(), which reads from Django settings (lines 73-99):
# posthog/personhog_client/client.py
_client: Optional[PersonHogClient] = None
_lock = threading.Lock()
def get_personhog_client() -> Optional[PersonHogClient]:
global _client
if _client is None:
with _lock:
if _client is None:
addr = getattr(settings, "PERSONHOG_ADDR", "")
if not addr:
return None
timeout_ms = getattr(settings, "PERSONHOG_TIMEOUT_MS", 5000)
client_name = getattr(settings, "OTEL_SERVICE_NAME", "posthog-django")
_client = PersonHogClient(
addr=addr,
client_name=client_name,
timeout_ms=timeout_ms,
)
return _client
Required Django settings include:
PERSONHOG_ADDR– The gRPC endpoint address (e.g.,"personhog:50051")PERSONHOG_TIMEOUT_MS– Request timeout in milliseconds (default: 5000)OTEL_SERVICE_NAME– Service name for OpenTelemetry tracing
Checking the Feature Gate
PersonHog adoption is controlled by a feature gate defined in posthog/personhog_client/gate.py. The use_personhog() function returns a boolean indicating whether to route requests to the gRPC service or fall back to the Django ORM.
Typical usage pattern (as seen in posthog/queries/actor_base_query.py, lines 311-316):
from posthog.personhog_client.gate import use_personhog
if use_personhog():
persons = _fetch_people_via_personhog(team.pk, people_ids, distinct_id_limit)
else:
# ORM fallback
persons = Person.objects.filter(...).all()
This gate allows gradual rollout and safe degradation if the PersonHog service becomes unavailable.
Fetching Person Data via gRPC
The client exposes several RPC methods for person access, defined in posthog/personhog_client/client.py (lines 77-140):
| RPC Method | Request Type | Response Type |
|---|---|---|
get_person |
GetPersonRequest |
GetPersonResponse |
get_person_by_distinct_id |
GetPersonByDistinctIdRequest |
GetPersonResponse |
get_persons_by_uuids |
GetPersonsByUuidsRequest |
PersonsResponse |
To fetch a person by distinct ID:
from posthog.personhog_client.proto import GetPersonByDistinctIdRequest
client = get_personhog_client()
request = GetPersonByDistinctIdRequest(team_id=team_id, distinct_id=distinct_id)
response = client.get_person_by_distinct_id(request)
All RPC methods apply the timeout configured during client initialization.
Accessing Group Data
PersonHog also handles group information. Available methods include:
| RPC Method | Request Type | Response Type |
|---|---|---|
get_group |
GetGroupRequest |
GetGroupResponse |
get_group_type_mappings_by_team_id |
GetGroupTypeMappingsByTeamIdRequest |
GroupTypeMappingsResponse |
Group type mappings are commonly retrieved using the helper function fetch_group_type_mapping_result defined in posthog/personhog_client/converters.py (lines 79-97).
Converting Protobuf to Django Models
RPC responses return protobuf messages that must be converted to Django model instances. The conversion utilities reside in posthog/personhog_client/converters.py.
Convert a person protobuf to a Django model (lines 54-77):
def proto_person_to_model(person: person_pb2.Person, distinct_ids: list[str] | None = None) -> Person:
from posthog.models.person import Person as PersonModel
obj = PersonModel(
id=person.id,
uuid=uuid_mod.UUID(person.uuid) if person.uuid else None,
team_id=person.team_id,
properties=json.loads(person.properties) if person.properties else {},
is_identified=person.is_identified,
created_at=datetime.fromtimestamp(person.created_at / 1000, tz=UTC),
last_seen_at=datetime.fromtimestamp(person.last_seen_at / 1000, tz=UTC) if person.last_seen_at else None,
)
if distinct_ids is not None:
obj._distinct_ids = distinct_ids
return obj
For group data, use proto_group_type_mapping_to_result (lines 47-52) to return lightweight dataclasses suitable for viewset responses.
Complete Example: Loading a Person
The following pattern appears in posthog/session_recordings/models/session_recording.py and posthog/queries/actor_base_query.py:
from posthog.personhog_client.gate import use_personhog
from posthog.personhog_client.client import get_personhog_client
from posthog.personhog_client.proto import GetPersonByDistinctIdRequest
from posthog.personhog_client.converters import proto_person_to_model
from posthog.models.person import Person
def load_person(team_id: int, distinct_id: str) -> Person | None:
"""
Return a Person for distinct_id in team_id.
Uses PersonHog when enabled, otherwise falls back to ORM.
"""
if use_personhog():
client = get_personhog_client()
if client is None:
raise RuntimeError("PersonHog client not configured")
request = GetPersonByDistinctIdRequest(
team_id=team_id,
distinct_id=distinct_id
)
resp = client.get_person_by_distinct_id(request)
if resp.person:
return proto_person_to_model(resp.person, distinct_ids=[distinct_id])
return None
else:
try:
return Person.objects.get(
team_id=team_id,
_distinct_ids__contains=distinct_id
)
except Person.DoesNotExist:
return None
Testing with the Fake Client
Unit tests use an in-process fake client to avoid network calls. The implementation resides in posthog/personhog_client/fake_client.py.
Activate the fake client using the context manager:
from posthog.personhog_client.fake_client import fake_personhog_client
with fake_personhog_client():
# RPC calls are intercepted and recorded
client = get_personhog_client()
# ... make assertions on recorded calls
For parametrized test suites, use the mixin from posthog/personhog_client/test_helpers.py:
class PersonhogTestMixin:
def setUp(self):
if self.personhog:
self._personhog_cm = fake_personhog_client()
self._personhog_cm.__enter__()
def tearDown(self):
if self.personhog:
self._personhog_cm.__exit__(None, None, None)
Summary
- Initialize the client via
get_personhog_client()after configuringPERSONHOG_ADDRin Django settings; the client uses a thread-safe singleton pattern. - Gate traffic using
use_personhog()fromposthog/personhog_client/gate.pyto determine whether to use gRPC or the Django ORM. - Fetch data through RPC methods like
get_person_by_distinct_id()orget_group_type_mappings_by_team_id(). - Convert responses to Django models using
proto_person_to_model()and related helpers inposthog/personhog_client/converters.py. - Test safely using
fake_personhog_client()or the provided test mixin to intercept calls without network dependencies.
Frequently Asked Questions
What is PersonHog and why does PostHog use a gRPC client?
PersonHog is a dedicated microservice within the PostHog architecture that stores person and group information separately from the main Django application. The gRPC client in posthog/personhog_client provides a type-safe, performant way to query this service while maintaining observability through OpenTelemetry tracing and Prometheus metrics.
How do I enable the PersonHog client in my PostHog instance?
Set the PERSONHOG_ADDR Django setting to the gRPC endpoint address (e.g., "personhog:50051"). Optionally configure PERSONHOG_TIMEOUT_MS and OTEL_SERVICE_NAME. The use_personhog() gate function checks additional feature flags to determine whether to route traffic to the service.
What happens if the PersonHog gRPC service is unavailable?
The client includes timeout handling configured via PERSONHOG_TIMEOUT_MS (default 5000ms). The feature gate pattern allows the application to fall back to direct Django ORM queries when PersonHog is disabled or unreachable, ensuring graceful degradation of person and group data access.
How do I convert protobuf responses into usable Django objects?
Import conversion helpers from posthog/personhog_client/converters.py. Use proto_person_to_model() to transform Person protobuf messages into Django Person model instances, handling timestamp conversion, JSON parsing, and distinct ID assignment automatically. For group data, use fetch_group_type_mapping_result() to retrieve lightweight result objects.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →