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 configuring PERSONHOG_ADDR in Django settings; the client uses a thread-safe singleton pattern.
  • Gate traffic using use_personhog() from posthog/personhog_client/gate.py to determine whether to use gRPC or the Django ORM.
  • Fetch data through RPC methods like get_person_by_distinct_id() or get_group_type_mappings_by_team_id().
  • Convert responses to Django models using proto_person_to_model() and related helpers in posthog/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →