EventBridge duplicate events for conversation.start — deduplication failing with IdempotencyKey

We’re seeing duplicate conversation.start events hitting our Lambda consumer via EventBridge. The setup is standard: CXone event bridge rule → SQS → Lambda.

I’ve implemented idempotency in the Lambda using the IdempotencyKey from the event payload. The docs say this key is unique per event instance.

Here’s the event structure I’m parsing:

{
 "detail": {
 "eventType": "conversation.start",
 "IdempotencyKey": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
 "conversationId": "conv-123",
 "timestamp": "2023-10-27T14:30:00Z"
 }
}

My Lambda logic checks a DynamoDB table for the IdempotencyKey. If it exists, I return early. If not, I process and insert the key.

The issue: I’m getting two events with the exact same IdempotencyKey within 100ms. The first one processes fine. The second one arrives, checks DynamoDB, and… sometimes it misses the write from the first lambda due to eventual consistency or race conditions, leading to double processing.

I’ve tried:

  1. Using IdempotencyKey alone. (Fails on duplicates)
  2. Using conversationId + eventType. (Fails because conversation.start can legitimately happen multiple times if the conversation restarts or reconnects, though rare in our flow)
  3. Adding a timestamp to the composite key. (Fails because the timestamp is identical in the duplicate events)

Is the IdempotencyKey guaranteed to be unique across retries from the CXone side? Or is EventBridge itself retrying the same message?

If it’s a CXone source issue, how do I handle this? I can’t use conversationId alone for deduplication on conversation.start because we might have edge cases.

Maybe I need to use a different key combination? Or is there a setting in the EventBridge rule to filter duplicates?

Current Lambda for context:

def handler(event, context):
 idem_key = event['detail']['IdempotencyKey']
 if db.exists(idem_key):
 return {'statusCode': 200, 'body': 'Duplicate'}
 process_event(event)
 db.insert(idem_key)

What’s the reliable way to deduplicate these? The IdempotencyKey feels like it should work but doesn’t.

The IdempotencyKey in the EventBridge detail is actually just the event ID, which should be unique, but NICE CXone sometimes re-emits events during internal retries or state reconciliations. If you’re seeing duplicates with the same IdempotencyKey, that’s a platform bug you need to ticket. But more often, people hit this because they’re looking at the wrong level of granularity or the event is being triggered by multiple sources (like a transfer + start).

Wait, let me re-read your . You mentioned the key is unique per event instance. If the key is identical, your Lambda deduplication logic is likely failing silently, or SQS is delivering it twice before the visibility timeout clears.

Here’s the thing about EventBridge and SQS: SQS doesn’t guarantee exactly-once delivery by default unless you’re using FIFO queues. If you’re on Standard SQS, you will get duplicates. The IdempotencyKey is your safety net, but only if your Lambda actually checks it against a persistent store (like DynamoDB) or uses AWS Lambda Powertools.

If you’re using Powertools, make sure you’re decorating the handler correctly. A common mistake is putting the decorator on the wrong function or not handling the IdempotencyItemAlreadyExists exception properly.

import json
import boto3
from aws_lambda_powertools.utilities.idempotency import (
 idempotent,
 DynamoDBPersistenceLayer,
)
from aws_lambda_powertools.utilities.idempotency.serialization.json import JsonSerializer

# Initialize persistence layer
persistence_layer = DynamoDBPersistenceLayer(table_name="cxone-idempotency-store", 
 expiration=3600 * 24, # 1 day retention
 serializer=JsonSerializer())

@idempotent(persistence_store=persistence_layer, key="detail.eventType") # Use a composite key if needed
def lambda_handler(event, context):
 # This function will only execute once per unique key
 detail = event.get("detail", {})
 conversation_id = detail.get("conversation", {}).get("id")
 
 if not conversation_id:
 raise ValueError("Missing conversation ID")

 # Your actual processing logic here
 print(f"Processing conversation start: {conversation_id}")
 
 return {
 "statusCode": 200,
 "body": json.dumps({"message": "Event processed"})
 }

If you aren’t using Powertools, you’re probably doing a manual DynamoDB put with condition_expression="attribute_not_exists(IdempotencyKey)". That works, but it’s prone to race conditions if you don’t handle the ConditionalCheckFailedException correctly.

Also, check your EventBridge rule. Are you filtering on detail-type? If you’re catching all events and filtering in code, you might be picking up noise.

One more thing: CXone EventBridge events for conversation.start can fire multiple times if the conversation is routed across multiple queues or if there’s a wrap-up code change that triggers a state transition. Make sure you’re actually looking at the initial start event and not a state change that looks like a start.

Check the detail.conversation.routingState in the payload. If it’s queued vs connected, that might be your duplicate source.

Here’s a quick check you can run in your Lambda to log the raw event structure before deduplication:

import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
 # Log the full event for debugging duplicates
 logger.info(f"Raw Event: {event}")
 
 # Extract the IdempotencyKey
 idempotency_key = event.get("id") # EventBridge puts the unique ID at the root
 logger.info(f"IdempotencyKey: {idempotency_key}")
 
 # ... rest of your code

If the id at the root of the EventBridge event is the same, then SQS is the culprit. Standard SQS guarantees at-least-once delivery. You must implement deduplication in your consumer. The IdempotencyKey field in the detail object is just metadata from NICE, it’s not the SQS message ID.

Don’t rely on the NICE IdempotencyKey field in the detail object for SQS deduplication. Use the EventBridge id field at the root of the event object. That’s the globally unique identifier for that specific event emission.

If you’re still seeing duplicates with unique EventBridge IDs, then your Lambda is being invoked twice for the same message. Check your SQS visibility timeout. If your Lambda takes longer than the timeout, SQS makes the message visible again, and another instance picks it up. Increase the visibility timeout to match your Lambda’s timeout + buffer.

resource "aws_sqs_queue" "cxone_events" {
 name = "cxone-conversation-events"
 visibility_timeout_seconds = 300 # Increase if your Lambda takes longer than 5 mins
 message_retention_seconds = 345600
 
 redrive_policy = jsonencode({
 deadLetterTargetArn = aws_sqs_queue.dead_letter.arn
 maxReceiveCount = 3
 })
}

If you increase the visibility timeout and still see duplicates, then your Lambda is throwing an error mid-execution, causing SQS to redrive the message. Check your CloudWatch Logs for errors.

Also, make sure you’re acknowledging the message in SQS. If you’re using the SQS event source mapping in Lambda, it handles deletion automatically. If you’re polling SQS manually, you need to delete the message after processing.

This is usually a visibility timeout issue. Bump it up and watch the CloudWatch metrics for NumberOfMessagesSent vs ApproximateNumberOfMessagesVisible. If visible messages drop to zero after processing, you’re good. If they stay high, you’re not deleting them.

Let me know if you’re using Standard or FIFO SQS. FIFO solves this but adds cost and complexity with content-based deduplication.