In this post, we develop a simple real-time web application. Multiple users can simultaneously increment or decrement a counter, the result being immediately published to all users. Instead of retrieving the updated results via frequent HTTP GET requests (“excessive polling”), we are using a websocket API implemented in AWS API Gateway, with AWS Lambda and AWS DynamoDB as backend services to persist the state of the counter.
Application
The front-end consists of three elements, two buttons (+
and -
) and a rectangle that shows the current integer value of the counter:

Hence, the body of the HTML code is very simple:
<body>
<div class="buttons">
<div class="minus button">-</div>
<div class="value">?</div>
<div class="plus button">+</div>
</div>
</body>
The Websocket API
AWS API Gateway
not only allows you to create REST APIs that listen on HTTP requests but you can also create websocket
APIs that allow real-time communcation, like chat applications, multiplayer games etc.
Routes $connect
and $disconnect
A websocket gateway by default already contains two routes that are used when a connection is established ($connect
) or broken($disconnect
). They will be integrated with Lambda functions (as lambda proxies) that store connectionIds in a DynamoDB table on connection and delete these entries from the table once the connection is broken. Hence, by scanning the table we will be able get connection Ids for all currently existing connections.
For this purpose, we have created DynamoDB table websocketconnections
with (string-type) key connectionId
.
$connect
Upon establishing a connection we run lambda function WebsocketConnect
with the following lambda handler:
def lambda_handler(event, context):
table_name = 'websocketconnections'
item = {'connectionId': {'S': event['requestContext']['connectionId']}}
dynamodb_client = boto3.client('dynamodb')
dynamodb_client.put_item(TableName=table_name, Item=item)
response = {'statusCode': 200}
return response
$disconnect
Once a connection is broken, the corresponding entry is deleted from the table. WebsocketDisconnect
has the following lambda handler:
def lambda_handler(event, context):
table_name = 'websocketconnections'
item = {'connectionId': {'S': event['requestContext']['connectionId']}}
dynamodb_client = boto3.client('dynamodb')
dynamodb_client.delete_item(TableName=table_name, Key=item)
response = {'statusCode': 200}
return response
Routes plus
and minus
The users have two possible actions, they can click the plus or the minus button. This will increment or decrement a (global) counter. Note that all users share the same counter, hence each connected user can manipulate the counter and all users will share a common value of the counter.
To store the counter value, we create another DynamoDB table, websocketcounter
, with (string-type) key id
. The global counter is stored under id global-counter
.
def update_counter(action):
table_name = "websocketcounter"
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(table_name)
response1 = table.get_item(Key={'id': 'global-counter'})
curval = int(response1["Item"]['numcon'])
if action == 'plus':
newval = curval + 1
response = table.update_item(
Key={
'id': 'global-counter'
},
UpdateExpression="set numcon = numcon + :val",
ExpressionAttributeValues={
':val': Decimal(1)
},
ReturnValues="UPDATED_NEW"
)
elif action == 'minus':
newval = curval - 1
response = table.update_item(
Key={
'id': 'global-counter'
},
UpdateExpression="set numcon = numcon - :val",
ExpressionAttributeValues={
':val': Decimal(1)
},
ReturnValues="UPDATED_NEW"
)
else:
newval = 0
return newval
After we updated the value of the counter, we will need to send the updated value to all established connections. We get all current connections by scanning DynamoDB table websocketconnections
:
def get_connections():
dynamodb_client = boto3.client('dynamodb')
response = dynamodb_client.scan(TableName='websocketconnections',
ProjectionExpression='connectionId')
connections = [item['connectionId']['S'] for item in response['Items']]
return connections
Sending a message to a connection is easy with boto3
:
def post_all_connections(payload, connections):
api_client = boto3.client('apigatewaymanagementapi',
endpoint_url='https://g4o3kbiy4j.execute-api.eu-central-1.amazonaws.com/production')
for connection_id in connections:
api_client.post_to_connection(Data=json.dumps(payload).encode('utf-8'),
ConnectionId=connection_id)
return None
As we will handle both actions, plus and minus, in a single lambda function, called WebsocketCounter
, we can write the lambda handler as follows:
def lambda_handler(event, context):
# Update counter and persist
new_counter_value = update_counter(action=event["requestContext"]["routeKey"])
# Get all active connections
connections=get_connections()
# Construct payload
payload = {
"type": "state",
"value": new_counter_value
}
# Send the message to each connection
post_all_connections(payload=payload, connections=connections)
return {'statusCode': 200}
The Frontend Javascript
The JavaScript
establishes a websocket
connection to our API Gateway. It listens on click-events for the +
and -
buttons and sends a message to the websocket. Here, the message is simply just a JSON dictionary with key action
, whose value will be "plus"
or "minus"
, respectively. Furthermore, it listens for messages from the websocket and updates the text in the rectangle if the message contains key type
with value "state"
. The value itself is retrieved from the message’s key value
.
var minus = document.querySelector('.minus'),
plus = document.querySelector('.plus'),
value = document.querySelector('.value'),
websocket = new WebSocket("wss://g4o3kbiy4j.execute-api.eu-central-1.amazonaws.com/production");
minus.onclick = function (event) {
websocket.send(JSON.stringify({action: 'minus'}));
}
plus.onclick = function (event) {
websocket.send(JSON.stringify({action: 'plus'}));
}
websocket.onmessage = function (event) {
data = JSON.parse(event.data);
if (data.type == 'state') {
value.textContent = data.value;
} else {
console.error("unsupported event", data);
}
};