---
title: Notification of changes
description: Applications that need change notification can use a persistent search or read the external change log.
component: pingds
version: 8.1
page_id: pingds:ldap-guide:change-notification
canonical_url: https://docs.pingidentity.com/pingds/8.1/ldap-guide/change-notification.html
revdate: 2025-10-22T14:42:39Z
keywords: ["Change Notification", "LDAP"]
section_ids:
  persistent-search: Use persistent search
  use-ecl: Use the external change log
---

# Notification of changes

Applications that need change notification can use a persistent search or read the external change log.

## Use persistent search

Defined in the Internet-Draft, [Persistent Search: A Simple LDAP Change Notification Mechanism](https://datatracker.ietf.org/doc/html/draft-ietf-ldapext-psearch), a persistent search is like a regular search that never stops returning results. Every time a change happens in the scope of the search, the server returns an additional response:

1. Grant access to perform a persistent search, by adding an access control instruction (ACI) *(tooltip: \<div class="paragraph">
   \<p>An instruction or rule that can be used to grant or deny access to users to perform operations on a server.\</p>
   \</div>)* to use the persistent search control.

   Persistent searches consume server resources, so servers do not allow them by default. If an application does not have access, the request fails with an unavailable critical extension error:

   ```
   The LDAP search request failed: 12 (Unavailable Critical Extension)
   Additional Information:  The request control with Object Identifier (OID) "2.16.840.1.113730.3.4.3"
   cannot be used due to insufficient access rights
   ```

   The following command grants access under `dc=example,dc=com` to `My App`:

   ```console
   $ ldapmodify \
    --hostname localhost \
    --port 1636 \
    --useSsl \
    --trustStorePath /path/to/opendj/config/keystore \
    --trustStoreType PKCS12 \
    --trustStorePassword:file /path/to/opendj/config/keystore.pin \
    --bindDN uid=admin \
    --bindPassword password << EOF
   dn: dc=example,dc=com
   changetype: modify
   add: aci
   aci: (targetcontrol = "PSearch")
    (version 3.0;acl "Allow Persistent Search for My App";
    allow (read)(userdn = "ldap:///cn=My App,ou=Apps,dc=example,dc=com");)
   EOF
   ```

2. Start the persistent search.

   The following example initiates a persistent search, where notifications are sent for all update operations, only notifications about changed entries are returned, and no additional information are returned:

   ```console
   $ ldapsearch \
    --hostname localhost \
    --port 1636 \
    --useSsl \
    --trustStorePath /path/to/opendj/config/keystore \
    --trustStoreType PKCS12 \
    --trustStorePassword:file /path/to/opendj/config/keystore.pin \
    --bindDN 'cn=My App,ou=Apps,dc=example,dc=com' \
    --bindPassword password \
    --baseDN dc=example,dc=com \
    --persistentSearch ps:all:true:false \
    '(&)' >> /tmp/psearch.txt &
   $ export PSEARCH_PID=$!
   ```

   Notice the search filter, `(&)`, which is always true, meaning that it matches all entries. For details on settings for a persistent search, refer to the `--persistentSearch` option in [ldapsearch Options](../tools-reference/ldapsearch.html#ldapsearch-options).

3. Make changes that impact the persistent search results.

   > **Collapse: Show commands**
   >
   > To prepare to modify an entry, save the following as the LDIF file, [description.ldif](../_attachments/ldif/description.ldif):
   >
   > ```ldif
   > dn: uid=bjensen,ou=People,dc=example,dc=com
   > changetype: modify
   > replace: description
   > description: Hello, persistent search
   > ```
   >
   > The following commands perform a modify operation and a delete operation:
   >
   > ```console
   > $ ldapmodify \
   >  --hostname localhost \
   >  --port 1636 \
   >  --useSsl \
   >  --trustStorePath /path/to/opendj/config/keystore \
   >  --trustStoreType PKCS12 \
   >  --trustStorePassword:file /path/to/opendj/config/keystore.pin \
   >  --bindDN uid=kvaughan,ou=People,dc=example,dc=com \
   >  --bindPassword bribery << EOF
   > dn: uid=bjensen,ou=People,dc=example,dc=com
   > changetype: modify
   > replace: description
   > description: Hello, persistent search
   > EOF
   > $ ldapdelete \
   >  --hostname localhost \
   >  --port 1636 \
   >  --useSsl \
   >  --trustStorePath /path/to/opendj/config/keystore \
   >  --trustStoreType PKCS12 \
   >  --trustStorePassword:file /path/to/opendj/config/keystore.pin \
   >  --bindDN uid=kvaughan,ou=People,dc=example,dc=com \
   >  --bindPassword bribery \
   >  uid=tpierce,ou=People,dc=example,dc=com
   > ```

   > **Collapse: Show persistent search results**
   >
   > The result is the following responses to the persistent search:
   >
   > ```ldif
   > dn: uid=bjensen,ou=People,dc=example,dc=com
   > objectClass: person
   > objectClass: cos
   > objectClass: oauth2TokenObject
   > objectClass: inetOrgPerson
   > objectClass: organizationalPerson
   > objectClass: posixAccount
   > objectClass: top
   > classOfService: bronze
   > cn: Barbara Jensen
   > cn: Babs Jensen
   > description: Hello, persistent search
   > facsimileTelephoneNumber: +1 408 555 1992
   > gidNumber: 1000
   > givenName: Barbara
   > homeDirectory: /home/bjensen
   > l: San Francisco
   > mail: bjensen@example.com
   > manager: uid=trigden, ou=People, dc=example,dc=com
   > oauth2Token: {"access_token":"123","expires_in":59,"token_type":"Bearer","refresh_token":"456"}
   > ou: Product Development
   > ou: People
   > preferredLanguage: en, ko;q=0.8
   > roomNumber: 0209
   > sn: Jensen
   > telephoneNumber: +1 408 555 1862
   > uid: bjensen
   > uidNumber: 1076
   > userPassword: {PBKDF2-HMAC-SHA256}10:<hash>
   > dn: uid=tpierce,ou=People,dc=example,dc=com
   > objectClass: person
   > objectClass: cos
   > objectClass: inetOrgPerson
   > objectClass: organizationalPerson
   > objectClass: posixAccount
   > objectClass: top
   > classOfService: gold
   > cn: Tobias Pierce
   > departmentNumber: 1000
   > description: Description on ou=People
   > diskQuota: 100 GB
   > facsimileTelephoneNumber: +1 408 555 9332
   > gidNumber: 1000
   > givenName: Tobias
   > homeDirectory: /home/tpierce
   > l: Bristol
   > mail: tpierce@example.com
   > mailQuota: 10 GB
   > manager: uid=scarter, ou=People, dc=example,dc=com
   > ou: Accounting
   > ou: People
   > preferredLanguage: en-gb
   > roomNumber: 1383
   > sn: Pierce
   > street: Broad Quay House, Prince Street
   > telephoneNumber: +1 408 555 1531
   > uid: tpierce
   > uidNumber: 1042
   > userPassword: {PBKDF2-HMAC-SHA256}10:<hash>
   > ```
   >
   > If the data is replicated, the results include the entry `dc=example,dc=com`. Replication updates the `ds-sync-*` operational attributes on `dc=example,dc=com`, and those changes appear in the results because the entry is in the scope of the persistent search.

4. Terminate the persistent search.

   Interrupt the command with `CTRL`+`C` (`SIGINT`) or `SIGTERM`:

   ```console
   $ kill -s SIGTERM $PSEARCH_PID
   ```

## Use the external change log

You read the external change log over LDAP. When you poll the change log, you can get the list of updates that happened since your last request.

The external change log mechanism uses an LDAP control with OID `1.3.6.1.4.1.26027.1.5.4`. This control allows the client application to bookmark the last changes observed. The control returns a cookie that the application sends to the server to read the next batch of changes.

These steps show the client binding as directory superuser (`uid=admin`) to read the change log. Other accounts require sufficient access and privileges to read the change log. For instructions, refer to [Let a user read the changelog](../config-guide/changelog.html#read-ecl-as-regular-user):

1. Send an initial search request using the LDAP control with no cookie value.

   In this example, two changes appear in the changelog:

   ```console
   $ ldapsearch \
    --hostname localhost \
    --port 1636 \
    --useSsl \
    --trustStorePath /path/to/opendj/config/keystore \
    --trustStoreType PKCS12 \
    --trustStorePassword:file /path/to/opendj/config/keystore.pin \
    --bindDN uid=admin \
    --bindPassword password \
    --baseDN cn=changelog \
    --control "ecl:true" \
    "(&)" \
    changes changeLogCookie targetDN
   ```

   > **Collapse: Show output**
   >
   > ```
   > dn: cn=changelog
   >
   > dn: replicationCSN=<CSN1>,dc=example,dc=com,cn=changelog
   > changes:: <base64Changes1>
   > targetDN: uid=bjensen,ou=People,dc=example,dc=com
   > changeLogCookie: <COOKIE1>
   >
   > dn: replicationCSN=<CSN2>,dc=example,dc=com,cn=changelog
   > changes:: <base64Changes2>
   > targetDN: uid=bjensen,ou=People,dc=example,dc=com
   > changeLogCookie: <COOKIE2>
   > ```

   The changes are base64-encoded. You can decode them using the `base64` command. This example decodes a change:

   ```console
   $ base64 decode --encodedData cmVwbGFjZTogZGVzY3JpcHRpb24KZGVzY3JpcHRpb246IE5ldyBkZXNjcmlwdGlvbgotCnJlcGxhY2U6IG1vZGlmaWVyc05hbWUKbW9kaWZpZXJzTmFtZTogdWlkPWJqZW5zZW4sb3U9UGVvcGxlLGRjPWV4YW1wbGUsZGM9Y29tCi0KcmVwbGFjZTogbW9kaWZ5VGltZXN0YW1wCm1vZGlmeVRpbWVzdGFtcDogMjAxNjEwMTQxNTA5MTJaCi0K
   ```

   > **Collapse: Show output**
   >
   > ```
   > replace: description
   > description: New description
   > -
   > replace: modifiersName
   > modifiersName: uid=bjensen,ou=People,dc=example,dc=com
   > -
   > replace: modifyTimestamp
   > modifyTimestamp: <timestamp>
   > -
   > ```

2. To start reading a particular change in the changelog, provide the cookie with the control:

   ```console
   $ ldapsearch \
    --hostname localhost \
    --port 1636 \
    --useSsl \
    --trustStorePath /path/to/opendj/config/keystore \
    --trustStoreType PKCS12 \
    --trustStorePassword:file /path/to/opendj/config/keystore.pin \
    --bindDN uid=admin \
    --bindPassword password \
    --baseDN cn=changelog \
    --control "ecl:true:$COOKIE1" \
    "(&)" \
    changes changeLogCookie targetDN
   ```

   > **Collapse: Show output**
   >
   > ```
   > dn: replicationCSN=<CSN2>,dc=example,dc=com,cn=changelog
   > changes:: <base64Changes2>
   > targetDN: uid=bjensen,ou=People,dc=example,dc=com
   > changeLogCookie: <COOKIE2>
   > ```

   This command decodes the change returned:

   ```console
   $ base64 decode --encodedData cmVwbGFjZTogZGVzY3JpcHRpb24KZGVzY3JpcHRpb246IE5ldywgaW1wcm92ZWQgZGVzY3JpcHRpb24KLQpyZXBsYWNlOiBtb2RpZmllcnNOYW1lCm1vZGlmaWVyc05hbWU6IHVpZD1iamVuc2VuLG91PVBlb3BsZSxkYz1leGFtcGxlLGRjPWNvbQotCnJlcGxhY2U6IG1vZGlmeVRpbWVzdGFtcAptb2RpZnlUaW1lc3RhbXA6IDIwMTYxMDE0MTUwOTE5WgotCg==
   ```

   > **Collapse: Show output**
   >
   > ```
   > replace: description
   > description: New, improved description
   > -
   > replace: modifiersName
   > modifiersName: uid=bjensen,ou=People,dc=example,dc=com
   > -
   > replace: modifyTimestamp
   > modifyTimestamp: <timestamp>
   > -
   > ```

3. If you lose the cookie, start over from the earliest available change by sending a request with no cookie.
