AWS EventBridge rule firing duplicate Genesys Cloud events despite deduplication logic

We’ve got a custom agent desktop extension listening to conversation:call:status events via AWS EventBridge. The integration works, but we’re seeing double callbacks for every single state change. I’ve checked the EventBridge rule and the target Lambda is definitely configured to handle the payload. I added a deduplication check in the Lambda handler using a Redis cache with a 10-second TTL on the conversationId, but it’s still processing the event twice within the same second. The first invocation hits the cache miss, sets the key, and processes. The second invocation arrives before the first one finishes, sees the cache hit, but for some reason, it still proceeds to make the API call to update our internal DB. Here’s the relevant snippet from the handler:

const isDuplicate = await redis.get(`call:${event.detail.conversationId}`);
if (isDuplicate) { return { statusCode: 200, body: 'Duplicate ignored' }; }
await redis.setex(`call:${event.detail.conversationId}`, 10, '1');
// process event

The logs show two separate invocations with identical detail.arn and timestamps down to the millisecond. I’ve verified the Genesys Cloud webhook endpoint isn’t sending duplicates by checking the raw EventBridge input. It feels like EventBridge is retrying the same event immediately after a 200 response, or maybe the async nature of the first call is causing a race condition where the setex hasn’t committed when the second check runs. I’ve tried adding exponential backoff in the Lambda but that just delays the duplicate processing. Anyone seen this specific behavior with Genesys Cloud event streams? Is there a specific header or property in the EventBridge payload I should be hashing instead of just the ID?

You’re fighting the wrong layer. EventBridge doesn’t duplicate. The source does.

Genesys Cloud sends a status event when the state changes, then often sends a second event shortly after if the participant count changes or a sub-state updates (like answered vs connected). If you’re just hashing conversationId, both events look unique to your dedup logic because the payload timestamp or sequence ID differs.

Switch to hashing the specific action + state + timestamp window. Or better, use the event.id from the Genesys payload. It’s unique per event instance.

import hashlib
import time

def handle_event(event):
 # Genesys payload structure
 payload = event['detail']
 
 # Create a dedup key using the unique event ID provided by Genesys
 # This is more reliable than conversationId alone
 event_id = payload.get('id')
 if not event_id:
 # Fallback if ID is missing (rare in v2 events)
 event_id = hashlib.md5(f"{payload['conversationId']}_{payload['state']}_{payload['timestamp']}".encode()).hexdigest()
 
 # Check Redis
 cache_key = f"genesys_event:{event_id}"
 if redis_client.exists(cache_key):
 return {"statusCode": 200, "body": "Duplicate ignored"}
 
 # Set with 60s TTL to cover any late duplicates
 redis_client.setex(cache_key, 60, "1")
 
 # Process event
 process_conversation_change(payload)

Also check your EventBridge rule target configuration. If you have retry policies enabled with “bisect” strategy, and your Lambda returns a 5xx error on the first pass, EventBridge will retry immediately. Make sure your Lambda returns 200 even if it decides to ignore the duplicate. A 500 triggers a retry, which looks like a duplicate execution.

Check the CloudWatch logs for the Lambda. If you see two invocations with different requestIds but identical payloads within milliseconds, it’s a retry. If the payloads differ slightly (e.g., different timestamp or sequence), it’s a double emission from Genesys.

The warning about the source layer is spot on. You’re likely catching the initial status change and then a subsequent participant update. Hashing just the conversationId isn’t enough because the event sequence numbers differ.

In scripts, we usually handle this by checking the event type and timestamp delta. If you’re doing this in Lambda, look at the event.detail.conversationId and event.detail.timestamp. If the timestamp difference is under 500ms and the status hasn’t actually changed (e.g. connected to connected), drop it.

Also, check your EventBridge rule’s input transformer. Sometimes it maps the whole payload incorrectly, causing the Lambda to see a different object structure for the “same” logical event. Ensure you’re using $.detail directly.

Here’s a quick check in Python for your Lambda:

def is_duplicate(current_event, last_event):
 if current_event['detail']['conversationId'] != last_event['detail']['conversationId']:
 return False
 time_diff = abs(current_event['detail']['timestamp'] - last_event['detail']['timestamp'])
 return time_diff < 0.5 # 500ms threshold

Don’t rely on Redis for sub-second dedup if the network latency varies. It’s messy.