# How to Use the PersonHog gRPC Client for Person and Group Data Access in PostHog

> Learn to use the PersonHog gRPC client for efficient person and group data access in PostHog. This guide covers initialization, feature gate checks, RPC calls, and model conversion.

- Repository: [PostHog/posthog](https://github.com/PostHog/posthog)
- Tags: how-to-guide
- Published: 2026-04-25

---

**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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/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:

```python

# 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):

```python

# 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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/posthog/queries/actor_base_query.py), lines 311-316):

```python
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`](https://github.com/PostHog/posthog/blob/main/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:

```python
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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/posthog/personhog_client/converters.py)**.

Convert a person protobuf to a Django model (lines 54-77):

```python
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`](https://github.com/PostHog/posthog/blob/main/posthog/session_recordings/models/session_recording.py) and [`posthog/queries/actor_base_query.py`](https://github.com/PostHog/posthog/blob/main/posthog/queries/actor_base_query.py):

```python
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`](https://github.com/PostHog/posthog/blob/main/posthog/personhog_client/fake_client.py)**.

Activate the fake client using the context manager:

```python
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`](https://github.com/PostHog/posthog/blob/main/posthog/personhog_client/test_helpers.py)**:

```python
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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/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`](https://github.com/PostHog/posthog/blob/main/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.