Skip to main content
Custom encryption lets Agent Server use your encryption handlers for per-tenant keys and external key management systems. Use built-in AES encryption unless you need these capabilities.
Agent Server versions 0.5.34–0.6.21 included a pre-release version of custom encryption. Data encrypted with these versions will be corrupted when upgrading to 0.6.22+. Do not use custom encryption on these versions.
Only use custom encryption if built-in AES encryption does not meet your needs. Custom encryption requires you to implement and maintain encryption handlers, and adds operational complexity. If you only need a static key, key rotation, or optional selective field encryption, use built-in AES encryption instead.
Use custom encryption when you need:
  • Per-tenant key isolation—different encryption keys for different customers
  • KMS integration—AWS KMS, Google Cloud KMS, or HashiCorp Vault for key management, rotation, and audit logging

Configure custom encryption

How it works

  1. Configure the encryption module path in langgraph.json
  2. Define an encryption context handler that derives values such as a tenant ID from the authenticated user
  3. Define your encryption module with handlers for blob and JSON encryption
  4. Agent Server calls your handlers before storing and after retrieving data
For production deployments with key rotation and audit logging, see Envelope encryption with AWS Encryption SDK.

Configuration

Add your encryption module to langgraph.json:
If you’re migrating from basic encryption, keep LANGGRAPH_AES_KEY configured. Custom encryption handles new writes while existing AES-encrypted data remains readable.

Define encryption context

Use @encryption.context to derive encryption context from the authenticated user:
This handler runs once per request after authentication. The returned dictionary becomes ctx.metadata for every encryption operation in the request and is stored in plaintext so Agent Server can restore it during decryption. Include only non-secret identifiers and routing metadata, never keys, tokens, or credentials.

Defining your encryption module

Blob encryption (checkpoints)

Blob handlers encrypt checkpoint data—the serialized state from graph execution. Here’s a simplified example using per-tenant keys with Fernet (a symmetric encryption scheme from the cryptography library):
The ctx.metadata dictionary comes from the context handler, so each encryption handler can select the correct key.

JSON encryption (metadata)

JSON handlers encrypt structured data like thread metadata, assistant context, and run kwargs. Unlike blob encryption, you choose which fields to encrypt—keeping some unencrypted for search and filtering.

JSON encryption considerations

Encrypted fields cannot be searched or filtered. Design your metadata schema so that fields you need to query remain unencrypted.
JSON encryptors must preserve key structure. SQL JSONB merge operations work at the key level. Encryptors that change keys—whether by consolidating fields (e.g., moving sensitive data into __encrypted__) or by encrypting key names themselves—cause data loss during merges. Use per-key encryption: transform values in-place while preserving keys.
Migration consideration: Use a recognizable prefix or format in encrypted values so your decryptor can detect and skip unencrypted data. This allows you to encrypt additional fields in the future without re-encrypting existing records. The example above uses this pattern.
Performance consideration: Per-key encryption means one encryption call per field. If your encryption involves round-trips to an external service (e.g., KMS), this can significantly impact latency. Consider caching data keys locally or using envelope encryption where you encrypt a local data key with KMS and use it for multiple fields.
Fields used by authorization filters (e.g., tenant_id, owner) must remain unchanged and unencrypted, as must fields used for search and filtering. Agent Server rejects writes if a custom encryptor changes an authorization-filter field. Additionally, some system-managed fields will never be encrypted:
  • Resource identifiers (thread_id, run_id, assistant_id, graph_id, checkpoint_id, task_id)
  • Named LangGraph system fields (langgraph_version, langgraph_api_version, langgraph_plan, langgraph_host, langgraph_api_url, langgraph_request_id, langgraph_auth_user, langgraph_auth_user_id, langgraph_auth_permissions)
  • Required checkpoint metadata (source, step, parents, run_attempt)
  • Named internal fields used for scheduling and orchestration, including __after_seconds__ and __request_start_time_ms__
  • Run-level execution limits (max_concurrency, recursion_limit) specified in a run’s config
  • Thread TTL updates (ttl) specified in a run’s config.configurable

What gets encrypted

JSON handlers (@encryption.encrypt.json / @encryption.decrypt.json) are applied recursively to fields including:
  • thread.metadata, thread.values
  • assistant.metadata, assistant.context
  • run.metadata, run.kwargs
  • cron.metadata, cron.payload
  • store.value
Some fields are excluded from encryption. Unless otherwise noted, these exclusions apply at every level of a nested JSON object, not just the root level. Blob handlers (@encryption.encrypt.blob / @encryption.decrypt.blob) are applied to checkpoint blobs (graph execution state).

Rotate custom encryption keys lazily

Lazy re-encryption requires Agent Server version 0.14.0 or later, Python SDK version langgraph-sdk>=0.4.3, the PostgreSQL checkpointer, PREFER_GRPC_CHECKPOINTER=true, and LANGGRAPH_STORE_BACKEND=grpc.
Return DecryptResult from a blob or JSON decrypt handler when the handler decrypts data with an old key. Its plaintext value is returned to the caller, and its replacement value is new ciphertext for Agent Server to persist. For example, replace the blob decrypt handler above with one that tries the current tenant key first, then returns replacement ciphertext when a legacy key succeeds:
JSON decrypt handlers can return DecryptResult[dict] in the same way. The replacement must have the same shape as a normal result from the corresponding encrypt handler. Agent Server writes the replacement with a compare-and-swap operation, so it does not overwrite a concurrent update. Re-encryption is best effort: a read still succeeds if the stored value changes before writeback or if writeback fails.
Lazy re-encryption only updates data as it is read. Keep old keys available while unread data may still require them.

Monitor lazy re-encryption

Agent Server emits the following INFO-tier internal metrics for replacement writebacks: Each metric includes reencryption_table and reencryption_column attributes. Use them to find surfaces that still read old ciphertext and to isolate writeback failures. CAS skips produce warning logs with Re-encryption writeback skipped due to concurrent update. Failures produce error logs with Re-encryption writeback failed. Both include the table and column. Successful writebacks do not produce logs. For Prometheus, set EXPOSE_INTERNAL_METRICS_PROMETHEUS=true and scrape the Agent Server /metrics endpoint. Internal metrics are also available through the documented Datadog export. These counters measure writeback outcomes, not the amount of old ciphertext remaining. A period without new writebacks does not prove that every record has been read and rotated. Keep old keys available until your retention policy or a separate exhaustive migration confirms that no unread data requires them.

Envelope encryption with AWS Encryption SDK

For production deployments on AWS, use the AWS Encryption SDK with AWS KMS, or an equivalent within your cloud provider. This approach:
  • Handles envelope encryption automatically (no manual key packing)
  • Provides key rotation and audit logging
  • Binds ciphertext to encryption context (tenant isolation)
  • Caches data keys locally to avoid repeated KMS calls, latency and rate limits

Complete example

The encryption_context is cryptographically bound to the ciphertext via KMS—decryption fails if the context doesn’t match. The context is embedded in the ciphertext, so decrypt handlers don’t need to reference ctx.metadata.

Key rotation

KMS rotates backing material for the same key ID automatically. Old encrypted data keys remain decryptable, so existing data does not need re-encryption.