Serverless DNA Logo
Loading...
Lambda Durable Functions - Keeping your Payloads Secure hero image

Lambda Durable Functions - Keeping your Payloads Secure

Lambda Durable Functions store your checkpoint data safely and securely, but do you really want all your system operators to see credit card numbers?

AWS Lambda Durable Functions represent a significant evolution in how we build serverless workflows. Instead of managing complex state machines or external orchestrators, developers can write straightforward sequential code that spans minutes, hours, or even days. The framework handles checkpointing, replay, and recovery automatically. However, this convenience introduces a security consideration that deserves careful attention: your workflow data gets persisted in checkpoints, and that data may contain sensitive information.

In this article we will explore the deeper parts of the Lambda Durable SDK and explore how you can go about securing your precious payload data.

What Are Durable Functions?

Lambda Durable Functions enable you to write long-running workflows as regular Lambda code. The durable execution SDK provides primitives like steps, waits, and callbacks that automatically create checkpoints of your function's progress. When your function pauses for a wait operation or gets interrupted by a failure, Lambda can resume exactly where it left off by replaying the checkpointed operations.

The checkpoint and replay mechanism works by storing the results of each durable operation. When your function executes a step that calls an external API, the SDK captures that step's return value in a checkpoint before continuing. If your function later pauses or fails, the next invocation replays from the beginning but uses the stored checkpoint results instead of re-executing completed steps. This approach allows your code to remain deterministic while spanning execution times far beyond Lambda's 15-minute invocation limit.

For a comprehensive introduction to durable functions, I recommend reading the AWS Lambda durable functions announcement and reviewing the official documentation.

Where Checkpoint Data Lives and the Security Model

Understanding where your checkpoint data gets stored is essential for implementing proper security controls. Lambda stores durable execution state in an AWS-managed service infrastructure. When you enable durable execution on a Lambda function, AWS manages all the checkpoint storage behind the scenes. Your function's execution role requires permissions for lambda:CheckpointDurableExecution and lambda:GetDurableExecutionState to interact with this storage, and a function execution is only able to access its own checkpoint data. The actual data lives in AWS-managed infrastructure, not in resources within your AWS account.

Each checkpoint contains the operation type, operation name, input parameters, output values, and metadata like timestamps and retry counts. For a payment processing workflow, this means the checkpoint storage contains customer payment details, credit card information, or other personally identifiable information depending on what data flows through your durable operations.

Lambda automatically encrypts checkpoint data at rest using AWS-owned encryption keys and in transit using TLS when Lambda reads or writes checkpoints. However, AWS does not currently support using customer-managed KMS keys (CMK) for checkpoint storage. You cannot control key rotation, access policies, or audit logging for the storage encryption itself. For many applications, this level of security is perfectly adequate. The data remains encrypted at rest and in transit, and access is controlled through IAM permissions on the checkpoint APIs.

However, organizations with strict compliance requirements around encryption key management need additional controls beyond what the default checkpoint storage provides. Healthcare applications subject to HIPAA, financial services handling payment card data under PCI-DSS, or government systems with data sovereignty requirements often mandate customer-controlled encryption keys. This is where custom serialization becomes essential and the Lambda Durable Functions team have provided SDK hooks for custom serialisation and deserialisation (SerDes) for payload checkpointing.

The AWS documentation explicitly states that when you implement custom encryption through serializers and deserializers, you lose visibility of operation results in the Lambda console and API responses. Checkpoint data appears encrypted in execution history and cannot be inspected without decryption. This trade-off between security and operational visibility is unavoidable with the current architecture. You must decide which is more important for your use case.

Full Payload Encryption

The recommendation is implementing custom encryption using the SDK's SerDes mechanism when you need additional security controls beyond the default encryption (source). This approach encrypts the entire step result before it reaches the checkpoint storage. You maintain full control over the encryption keys, can use customer-managed KMS keys, and can implement any encryption algorithm your compliance requirements demand.

The implementation involves creating a custom serializer class that encrypts data during checkpoint creation and decrypts it during replay.

TypeScript SerDes Implementation

import type { Serdes } from '@aws/durable-execution-sdk-js'; import { DecryptCommand, EncryptCommand, KMSClient } from '@aws-sdk/client-kms'; const kmsClient = new KMSClient({}); const KMS_KEY_ID = process.env.KMS_KEY_ID; // SerdesContext interface from the SDK (not exported in v1.0.1) interface SerdesContext { entityId: string; durableExecutionArn: string; } export class KmsSerDer<T> implements Serdes<T> { async serialize( value: T | undefined, context: SerdesContext, ): Promise<string | undefined> { if (value === undefined) { return undefined; } if (!KMS_KEY_ID) { throw new Error('KMS_KEY_ID environment variable is not set'); } const plaintext = JSON.stringify(value); const encoder = new TextEncoder(); const plaintextBytes = encoder.encode(plaintext); const command = new EncryptCommand({ KeyId: KMS_KEY_ID, Plaintext: plaintextBytes, EncryptionContext: { entityId: context.entityId, durableExecutionArn: context.durableExecutionArn, }, }); const response = await kmsClient.send(command); if (!response.CiphertextBlob) { throw new Error('Encryption failed: no ciphertext returned'); } // Convert to base64 for storage return Buffer.from(response.CiphertextBlob).toString('base64'); } async deserialize( encryptedData: string | undefined, context: SerdesContext, ): Promise<T | undefined> { if (encryptedData === undefined) { return undefined; } // Convert from base64 const ciphertextBlob = Buffer.from(encryptedData, 'base64'); const command = new DecryptCommand({ CiphertextBlob: ciphertextBlob, EncryptionContext: { entityId: context.entityId, durableExecutionArn: context.durableExecutionArn, }, }); const response = await kmsClient.send(command); if (!response.Plaintext) { throw new Error('Decryption failed: no plaintext returned'); } const decoder = new TextDecoder(); const plaintext = decoder.decode(response.Plaintext); return JSON.parse(plaintext) as T; } }

Applying SerDes to a Durable Step

Once you have created a class implementing the SerDes interface (shown above) you can apply that to each step where you want custom serialisation to be used.

// Use KMS encryption for the order step const order = await context.step<Order>( 'create-order', async () => { return createOrder(event); }, { serdes: kmsSerDes, }, );

The example implementation makes direct use of KMS keys which have an upper limit of 4KB on payload size for encryption, which may not be suitable for all payload types. Where you need to encrypt more than 4KB of data you will need to make use of the AWS encryption SDK and use envelope encryption.

Envelope encryption is where your data is encrypted with a unique data encryption key (DEK), and that DEK is then encrypted with your KMS master key. This approach means you can encrypt large amounts of data quickly with symmetric encryption while KMS only needs to encrypt/decrypt the small DEK, and the encrypted message contains both the encrypted data and the encrypted DEK together in one portable blob.

In the github link at the end of this article I have provided examples of both implementations - direct KMS and AWS Envelope encryption.

What This Looks Like in Production

When you view the durable execution in the Lambda console, the checkpoint data appears as an encrypted blob. The execution timeline shows that steps completed successfully, but you cannot see the actual data without decrypting it which reduces operational visibility and increases support friction.

Wthout Encryption

{ "EventId": 3, "EventTimestamp": "2026-01-21T12:53:09.678Z", "EventType": "StepSucceeded", "Id": "c4ca4238a0b92382", "Name": "create-order", "StepSucceededDetails": { "RetryDetails": { "CurrentAttempt": 1 }, "Result": { "Truncated": false, "Payload": "{\"customer\":{\"customer_id\":\"123456789\",\"name\":\"John Doe\",\"email\":\"john@example.com\",\"ssn\":\"123-45-6789\",\"address\":\"123 Main St\"},\"payment\":{\"method\":\"credit_card\",\"creditCard\":\"4111-1111-1111-1111\",\"amount\":100},\"items\":[{\"id\":\"123\",\"quantity\":2},{\"id\":\"456\",\"quantity\":2}]}" } }, "SubType": "Step" }

With Encryption

{ "EventId": 3, "EventTimestamp": "2026-01-21T13:46:04.182Z", "EventType": "StepSucceeded", "Id": "c4ca4238a0b92382", "Name": "create-order", "StepSucceededDetails": { "RetryDetails": { "CurrentAttempt": 1 }, "Result": { "Truncated": false, "Payload": "AQICAHjFMg8rj2ThqXWiaabEbpqoKCF9xVaTOZP6HOCD17VYhwHddxCX5VW6qI5ehwq4zFGyAAABADCB/QYJKoZIhvcNAQcGoIHvMIHsAgEAMIHmBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDOc8WQxUXh32Isp/sQIBEICBuJWXZ3ST7HPxVuZH3epnhKwcyr2llFzKlUQrzxHxbGkqvNc9o2uhsbzGCq0Lnxq5uAa8aVRYR3Q7ijoGWctSa0NdAIv1FxiUlk9lh7fOALzZuifPxrmcQL4LZTl/ZQBje9GUfNZ9bhI4UbdayCqD8tY6CdYzEWzI2xIoBlAPHdwSX4Y9MGVGFJD8kyhb/oSdJBQmqieqT1DcGsxAAOYEKEtsL/V9YOC4NbxKgnyR7j4YGg+Y+2ImvdU=" } }, "SubType": "Step" }

You can see that your workflow progressed through its steps, but the sensitive data remains encrypted and un-readable. This provides strong security guarantees at the cost of operational visibility. When troubleshooting a failed workflow, you cannot simply look at the console to see what data was being processed. You must decrypt the checkpoint data programmatically, which requires appropriate KMS permissions.

Field-Level Encryption: Preserving Structure for Operations

Full payload encryption solves the security problem but introduces a significant operational challenge. Once encrypted, your checkpoint data becomes completely opaque. You cannot determine which customer's order failed. You cannot identify patterns in failures across different payment amounts. The lack of visibility makes production operations substantially harder.

A more sophisticated approach involves encrypting only the sensitive fields while preserving the overall structure of your data. This technique provides the security benefits of encryption while maintaining the operational visibility needed for production systems. The key insight is that not all data in your checkpoints requires encryption. Order IDs, timestamps, workflow states, and similar metadata can remain in plaintext for operational purposes.

Field-level encryption works by replacing sensitive fields with placeholder objects and encrypting all the sensitive values together in a single operation. The encrypted blob and the modified structure both get stored in the checkpoint. When the workflow resumes, the SDK automatically decrypts the blob and restores the original field values. Despite encrypting multiple fields, this approach makes only a single KMS call, maintaining the performance benefit of batch encryption.

TypeScript Implementation

import type { Serdes } from '@aws/durable-execution-sdk-js'; import { buildClient, CommitmentPolicy, KmsKeyringNode, } from '@aws-crypto/client-node'; const KMS_KEY_ARN = process.env.KMS_KEY_ARN; // SerdesContext interface from the SDK (not exported in v1.0.1) interface SerdesContext { entityId: string; durableExecutionArn: string; } interface EncryptedPayload<T> { data: T; __encrypted_pii?: string; } /** * Field-level encryption SerDes using AWS Encryption SDK with envelope encryption * This provides better performance for large payloads by using data keys */ export class EnvelopeEncryptionSerDes<T> implements Serdes<T> { private fieldPaths: string[]; private encryptionContext: Record<string, string>; private client: ReturnType<typeof buildClient>; private keyring: KmsKeyringNode | null = null; /** * @param fieldPaths - Array of dot-notation paths to fields that should be encrypted * e.g., ['customer.ssn', 'payment.creditCard'] * @param encryptionContext - Additional encryption context */ constructor( fieldPaths: string[], encryptionContext: Record<string, string> = {}, ) { this.fieldPaths = fieldPaths; this.encryptionContext = encryptionContext; // Build the encryption client with commitment policy this.client = buildClient(CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT); } private getKeyring(): KmsKeyringNode { if (!this.keyring) { if (!KMS_KEY_ARN) { throw new Error('KMS_KEY_ARN environment variable is not set'); } // Create KMS keyring for envelope encryption this.keyring = new KmsKeyringNode({ generatorKeyId: KMS_KEY_ARN, }); } return this.keyring; } async serialize( value: T | undefined, context: SerdesContext, ): Promise<string | undefined> { if (value === undefined) { return undefined; } // Clone the object to avoid mutating the original const clonedValue = JSON.parse(JSON.stringify(value)); // Extract sensitive fields const sensitiveData: Record<string, unknown> = {}; for (const path of this.fieldPaths) { const fieldValue = this.getNestedValue(clonedValue, path); if (fieldValue !== undefined) { sensitiveData[path] = fieldValue; // Replace with marker this.setNestedValue(clonedValue, path, { __encrypted: path }); } } // Encrypt the sensitive data if any fields were found let encryptedPii: string | undefined; if (Object.keys(sensitiveData).length > 0) { const plaintext = JSON.stringify(sensitiveData); // Build encryption context const fullContext = { entityId: context.entityId, durableExecutionArn: context.durableExecutionArn, ...this.encryptionContext, }; // Encrypt using AWS Encryption SDK (envelope encryption) const { result } = await this.client.encrypt( this.getKeyring(), plaintext, { encryptionContext: fullContext, }, ); // Convert to base64 for storage encryptedPii = Buffer.from(result).toString('base64'); } // Build the final payload const payload: EncryptedPayload<unknown> = { ...clonedValue, }; if (encryptedPii) { payload.__encrypted_pii = encryptedPii; } return JSON.stringify(payload); } async deserialize( encryptedData: string | undefined, context: SerdesContext, ): Promise<T | undefined> { if (encryptedData === undefined) { return undefined; } const payload: EncryptedPayload<unknown> = JSON.parse(encryptedData); // If there's encrypted PII, decrypt it if (payload.__encrypted_pii) { const ciphertext = Buffer.from(payload.__encrypted_pii, 'base64'); // Decrypt using AWS Encryption SDK const { plaintext, messageHeader } = await this.client.decrypt( this.getKeyring(), ciphertext, ); // Verify encryption context matches const expectedContext = { entityId: context.entityId, durableExecutionArn: context.durableExecutionArn, ...this.encryptionContext, }; for (const [key, value] of Object.entries(expectedContext)) { if (messageHeader.encryptionContext[key] !== value) { throw new Error( `Encryption context mismatch for key ${key}: expected ${value}, got ${messageHeader.encryptionContext[key]}`, ); } } const plaintextString = plaintext.toString('utf8'); const sensitiveData: Record<string, unknown> = JSON.parse(plaintextString); // Restore the sensitive fields for (const [path, value] of Object.entries(sensitiveData)) { this.setNestedValue(payload, path, value); } // Remove the encryption metadata delete payload.__encrypted_pii; } return payload as T; } private getNestedValue(obj: unknown, path: string): unknown { const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === undefined || current === null) { return undefined; } current = (current as Record<string, unknown>)[part]; } return current; } private setNestedValue(obj: unknown, path: string, value: unknown): void { const parts = path.split('.'); let current = obj as Record<string, unknown>; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (current[part] === undefined || current[part] === null) { current[part] = {}; } current = current[part] as Record<string, unknown>; } current[parts[parts.length - 1]] = value; } }

Applying this to Workflow Steps

// Create dedicated SerDes using AWS Encryption SDK with envelope encryption const orderSerDes = new EnvelopeEncryptionSerDes<Order>( ['customer.ssn', 'payment.creditCard'], { service: 'order-processing', environment: 'production', }, ); ... // Use envelope encryption (AWS Encryption SDK) for the order step // Only customer.ssn and payment.creditCard will be encrypted const order = await context.step<Order>( 'create-order', async () => { return createOrder(event); }, { serdes: orderSerDes, }, );

What Field-Level Encryption Looks Like in the Console

With field-level encryption, the Lambda console displays a much more useful view of your checkpoint data. The execution history shows the workflow structure with sensitive fields clearly marked as encrypted.

{ "EventId": 3, "EventTimestamp": "2026-01-21T14:29:08.022Z", "EventType": "StepSucceeded", "Id": "c4ca4238a0b92382", "Name": "create-order", "StepSucceededDetails": { "RetryDetails": { "CurrentAttempt": 1 }, "Result": { "Truncated": false, "Payload": "{\"id\":\"order-1769005747947\",\"customerId\":\"123456789\",\"customer\":{\"customer_id\":\"123456789\",\"name\":\"John Doe\",\"email\":\"john@example.com\",\"ssn\":{\"__encrypted\":\"customer.ssn\"},\"address\":\"123 Main St\"},\"payment\":{\"method\":\"credit_card\",\"creditCard\":{\"__encrypted\":\"payment.creditCard\"},\"amount\":100},\"items\":[{\"id\":\"123\",\"quantity\":2},{\"id\":\"456\",\"quantity\":2}],\"createdAt\":\"2026-01-21T14:29:07.947Z\",\"__encrypted_pii\":\"AgV4x2uTyXhneXm5Lt2L3qRfCcsJ0WCJZxgt8uLZdSLxp9EBjgAFABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREE1aFpLbWdvUk5RYU9PM1h2VThnRjFqRW9raXErZXpBZUlVM2dnVmZxTDBpTHlOYWpNZFlzRXM3dE05YVdkUTl5dz09ABNkdXJhYmxlRXhlY3V0aW9uQXJuANdhcm46YXdzOmxhbWJkYTphcC1zb3V0aGVhc3QtMjo4NTk1Nzg2NTExMDI6ZnVuY3Rpb246TGFtYmRhRHVyYWJsZUZ1bmN0aW9uRXhhbS1FbnZlbG9wZVdvcmtmbG93RnVuY3Rpb24teTNPNndQM1d4bXBkOiRMQVRFU1QvZHVyYWJsZS1leGVjdXRpb24vYzkzNTk5ODgtMDQ4ZS00NTE5LWE0YTctNmU2ZWI2YzYwNjU4LzkwNWVkNjQ3LTc5OTctM2JmNi05Y2M4LWZiYjVkMTBhYjQ0YwAIZW50aXR5SWQAATEAC2Vudmlyb25tZW50AApwcm9kdWN0aW9uAAdzZXJ2aWNlABBvcmRlci1wcm9jZXNzaW5nAAEAB2F3cy1rbXMAUGFybjphd3M6a21zOmFwLXNvdXRoZWFzdC0yOjg1OTU3ODY1MTEwMjprZXkvYzVmNGNkYzMtMWYyMy00NDUyLWEwMDAtZTdjNDgxODM3MTliALgBAgEAeMUyDyuPZOGpdaJppsRumqgoIX3FVpM5k/oc4IPXtViHAQ4ueB4Z5NN/r6oUnaYcKKQAAAB+MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAwo28yWtu49aAyYS2ACARCAO825YL1fsuSZUxS1VcxP1CgsTtoIfc+rtwUVUKgSh1+UwvXxZTxN7RVEPu/IK7GIGY9QsDi3zPV6zxUaAgAAEAAebuVheeSBpQIdicZSiFGZliUt3IPJxLhFQ4974/vNuEWWFb7JD1RZNub7/iCA++7/////AAAAAQAAAAAAAAAAAAAAAQAAAEk4GWvzu2BZDrdKhUKFM3zR1A6b2I/0DR/Bfs+gdGWfBsn+m4CCCq/zsYElqc8wGroQS3bd1m3+0+Qea+wwRGv2vPBJD7RldQQPfNr4AwI22ckfiFQYwH65eABoMGYCMQCI+jB0SXbxSQs9Pyvil1sjjM0kkZ+YhWyxcirggWnLzE43xNfB2U2IarmN+k0kE+wCMQDSbQSTleKz4yzJG/AXcyUQ2ohcFFtYZcOzlsImr1a7tRgOfrJ2sRxYhQF64tPtMrw=\"" } }, "SubType": "Step" }

JSON formatted Payload

Encrypted fields have been replaced with an enryption placeholder rather than showing the actual data. The encrypted data is still in the payload but at the end in a single blob, the overall payload structure is maintained.

Using placeholder text enables robust decryption to occur using the actual object names rather than relying on order of fields which can be haphazard.

{ "id": "order-1769005747947", "customerId": "123456789", "customer": { "customer_id": "123456789", "name": "John Doe", "email": "john@example.com", "ssn": { "__encrypted": "customer.ssn" }, "address": "123 Main St" }, "payment": { "method": "credit_card", "creditCard": { "__encrypted": "payment.creditCard" }, "amount": 100 }, "items": [ { "id": "123", "quantity": 2 }, { "id": "456", "quantity": 2 } ], "createdAt": "2026-01-21T14:29:07.947Z", "__encrypted_pii": "AgV4x2uTyXhneXm5Lt2L3qRfCcsJ0WCJZxgt8uLZdSLxp9EBjgAFABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREE1aFpLbWdvUk5RYU9PM1h2VThnRjFqRW9raXErZXpBZUlVM2dnVmZxTDBpTHlOYWpNZFlzRXM3dE05YVdkUTl5dz09ABNkdXJhYmxlRXhlY3V0aW9uQXJuANdhcm46YXdzOmxhbWJkYTphcC1zb3V0aGVhc3QtMjo4NTk1Nzg2NTExMDI6ZnVuY3Rpb246TGFtYmRhRHVyYWJsZUZ1bmN0aW9uRXhhbS1FbnZlbG9wZVdvcmtmbG93RnVuY3Rpb24teTNPNndQM1d4bXBkOiRMQVRFU1QvZHVyYWJsZS1leGVjdXRpb24vYzkzNTk5ODgtMDQ4ZS00NTE5LWE0YTctNmU2ZWI2YzYwNjU4LzkwNWVkNjQ3LTc5OTctM2JmNi05Y2M4LWZiYjVkMTBhYjQ0YwAIZW50aXR5SWQAATEAC2Vudmlyb25tZW50AApwcm9kdWN0aW9uAAdzZXJ2aWNlABBvcmRlci1wcm9jZXNzaW5nAAEAB2F3cy1rbXMAUGFybjphd3M6a21zOmFwLXNvdXRoZWFzdC0yOjg1OTU3ODY1MTEwMjprZXkvYzVmNGNkYzMtMWYyMy00NDUyLWEwMDAtZTdjNDgxODM3MTliALgBAgEAeMUyDyuPZOGpdaJppsRumqgoIX3FVpM5k/oc4IPXtViHAQ4ueB4Z5NN/r6oUnaYcKKQAAAB+MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAwo28yWtu49aAyYS2ACARCAO825YL1fsuSZUxS1VcxP1CgsTtoIfc+rtwUVUKgSh1+UwvXxZTxN7RVEPu/IK7GIGY9QsDi3zPV6zxUaAgAAEAAebuVheeSBpQIdicZSiFGZliUt3IPJxLhFQ4974/vNuEWWFb7JD1RZNub7/iCA++7/////AAAAAQAAAAAAAAAAAAAAAQAAAEk4GWvzu2BZDrdKhUKFM3zR1A6b2I/0DR/Bfs+gdGWfBsn+m4CCCq/zsYElqc8wGroQS3bd1m3+0+Qea+wwRGv2vPBJD7RldQQPfNr4AwI22ckfiFQYwH65eABoMGYCMQCI+jB0SXbxSQs9Pyvil1sjjM0kkZ+YhWyxcirggWnLzE43xNfB2U2IarmN+k0kE+wCMQDSbQSTleKz4yzJG/AXcyUQ2ohcFFtYZcOzlsImr1a7tRgOfrJ2sRxYhQF64tPtMrw=" }

The __encrypted_pii field contains the secure JSON data as an object.

{ "customer.ssn": "123-45-6789", "payment.creditCard": "4111-1111-1111-1111" }

The JSON is then:

  1. Stringified to plaintext
  2. Encrypted with KMS (produces binary ciphertext)
  3. Base64 encoded for JSON storage

The operational benefits become immediately clear. You can see the order ID, customer name, email, and payment amount in plaintext. When troubleshooting a failed payment, you can identify the specific order and see the payment amount without needing to decrypt anything. You can identify patterns like failures occurring only for payments above certain amounts. The sensitive fields are clearly marked with the __encrypted placeholder, making it obvious what data is protected while keeping everything else visible for operational purposes.

Making This Readily Available

The field-level encryption pattern solves a real problem that many developers face when building production durable functions. Rather than requiring every team to implement their own version, this functionality belongs in a reusable utility. I have submitted an RFC proposal to extend the DataMasking utility in Python to include field level encryption and an RFC for Typescript Powertools to implement and expose the same utility with full feature parity (Data Masking not available in Typescript Powertools).

If you find this functionality valuable for your durable functions, I encourage you to review and support the RFCs. Your feedback and use cases will help shape the final implementation. Visit the Python RFC and TypeScript RFC to add your voice to the discussion. A thumbs up reaction on the RFC helps demonstrate community demand and prioritization for the Powertools maintainers.

The security of your checkpoint data matters. Whether you choose full encryption for maximum security or field-level encryption for operational visibility, having a well-tested, reusable implementation will make securing your durable functions significantly easier. The examples in this article provide working code you can use today, and the Powertools RFCs aim to make this functionality available as a maintained, tested utility for the entire serverless community.

Full source code is available on GitHub https://github.com/walmsles/lambda-durable-function-encryption