{"id":19854681,"url":"https://github.com/obytes/apigw-websocket-pusher","last_synced_at":"2025-05-02T01:30:35.257Z","repository":{"id":46080786,"uuid":"428185464","full_name":"obytes/apigw-websocket-pusher","owner":"obytes","description":"AWS API Gateway Websocket Asynchronous Notifications Pusher","archived":false,"fork":false,"pushed_at":"2021-11-24T10:30:38.000Z","size":1243,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":13,"default_branch":"main","last_synced_at":"2025-04-06T20:35:48.582Z","etag":null,"topics":["api-gateway","asyncio","aws","aws-lambda","notifications","python","sns","sqs"],"latest_commit_sha":null,"homepage":"https://www.obytes.com/blog/go-serverless-part-4-realtime-interactive-and-secure-applications-with-aws-websocket-api-gateway","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/obytes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-11-15T08:43:43.000Z","updated_at":"2024-09-20T23:33:30.000Z","dependencies_parsed_at":"2022-08-30T21:30:13.588Z","dependency_job_id":null,"html_url":"https://github.com/obytes/apigw-websocket-pusher","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obytes%2Fapigw-websocket-pusher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obytes%2Fapigw-websocket-pusher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obytes%2Fapigw-websocket-pusher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/obytes%2Fapigw-websocket-pusher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/obytes","download_url":"https://codeload.github.com/obytes/apigw-websocket-pusher/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251969247,"owners_count":21673182,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-gateway","asyncio","aws","aws-lambda","notifications","python","sns","sqs"],"created_at":"2024-11-12T14:10:07.417Z","updated_at":"2025-05-02T01:30:34.090Z","avatar_url":"https://github.com/obytes.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AWS API Gateway Websocket Asynchronous Pusher\n\n![Illustration](docs/images/pusher.gif)\n\nFast AWS API Gateway websockets notifications' pusher using Python AsyncIO for managing asynchronous and concurrent\nnon-blocking IO calls to DynamoDB connections store and API Gateway management API. making it suitable for broadcasting\nmessages to multiple users with a fast and cost-effective approach.\n\nYou can test the Pusher with [this demo application](https://github.com/obytes/apigw-websocket-pusher) or use the live \ndemo at https://sumu.kodhive.com\n\n## Features\n\n- **Multiple sources**: capable of receiving notification requests from SQS and SNS, which make it suitable for\n  processing single notification request from SNS and high number of notification requests in batches when polling \n  from SQS\n\n- **Multicast Notifications**: the pusher can send messages to only a subset of users (eg: users in a chat room).\n\n- **Broadcast Notifications**: sometimes you just want to send the same message for all connected users (eg: broad\n  announcements)\n\n- **Exclusion Notifications**: the pusher can broadcast messages to all users except a list of excluded users (eg: \n  online/offline  presence events can be sent to all except the originator)\n\n- **Stale Connections Pruning**: capable of detecting and deleting stale connections from DynamoDB connections store\n  in case API Gateway missed cleaning them.\n\n- **Asynchronous Processing**: the pusher is using AsyncIO to notify multiple users/connections concurrently to not\n  wait for inflight requests to DynamoDB/API Gateway so you don't pay AWS the waiting time 😎\n\n- **Batch Processing**: when using SQS as event source the Pushed will be able to process batches of notification\n  requests, also concurrently.\n\n- **Duplicate Users Detection**: able to detect duplicate users in a notification requests and make them unique set of\n  users. to avoid double notifications.\n\n## Requirements\n\n![Illustration](docs/images/arch.svg)\n\n- **DynamoDB Connections Store**: a DynamoDB table where users connections are stored, It should have a hash\n  key **`user_id`** and a range key **`connection_id`**, where every user can have multiple connections and the Pusher \n  can retrieve specific user connections by querying with the hash key.\n\n- **AWS API Gateway Websockets API**: an already configured Websocket API Gateway. the Pusher will use the connection\n  management endpoint passed as environment variable to post messages to users.\n  \n- **SNS Topic**: the SNS Topic that the pusher will subscribe to for notifications requests.\n\n- **SQS Queue**: the SQS Queue that the pusher will poll notifications requests from.\n\n## Notification Requests\n\nBackend applications can send notifications to AWS API Gateway Websocket connected users by sending a notification\nrequest to the service integrated with the Pusher (SNS|SQS), notifications requests should meet the following format:\n\nFor Multicast notifications, the message should be a JSON String that contains the list of users and the actual `data`:\n\n```python\nimport json\n\nmessage = {\n    \"users\": [\"783304b1-2320-44db-8f58-09c3035a686b\", \"a280aa41-d99b-4e1c-b126-6f39720633cc\"],\n    \"data\": {\"type\": \"notification\", \"message\": \"A message sent to multiple user\"}\n}\nmessage_to_send = json.dumps(message)\n```\n\nFor broadcast notifications, the same but do not provide users list or provide an empty users list:\n\n```python\nimport json\n\nmessage = {\n    \"data\": {\"type\": \"announcement\", \"message\": \"A broadcast to all users\"}\n}\nmessage_to_send = json.dumps(message)\n```\n\nFor exclusion notifications, instead of providing users list, provide a list of excluded users:\n\n```python\nimport json\n\nmessage = {\n    \"exclude_users\": [\"783304b1-2320-44db-8f58-09c3035a686b\"],\n    \"data\": {\n      \"type\": \"announcement\", \n      \"message\": {\n        \"user_id\": \"783304b1-2320-44db-8f58-09c3035a686b\", \n        \"status\": \"OFFLINE\"\n      }\n    }\n}\nmessage_to_send = json.dumps(message)\n```\n\n## Integration\n\nThe Pusher can accept notifications requests from multiple sources like SQS and SNS. every source has its advantages and\ndisadvantages when it comes to speed and cost.\n\n### Notification requests through SNS\n\nThe Pusher can subscribe to notifications SNS Topic, and whenever a backend applications Publish notification requests\nto SNS, the later will quickly notify the Pusher by sending the notification request to the subscribed Pusher Lambda.\n\nThis will result in a fast delivery because this approach does not introduce a polling mechanism and SNS will notify the\nPusher whenever a notification request is available. however, at scale SNS will trigger a Pusher Lambda Function for\nevery notification request and given that the Lambda Function Concurrent Invocations Limit is 1,000 per account (Can be\nincreased to 100,000 by support-ticket) notification requests will be throttled for large applications.\n\n\u003e Publish to SNS when you have small application with few users\n\n```python\nimport os\nimport json\nimport time\nimport boto3\n\nmessage = {\n    \"users\": [\"783304b1-2320-44db-8f58-09c3035a686b\", \"a280aa41-d99b-4e1c-b126-6f39720633cc\"],\n    \"data\": {\n        \"type\": \"notification\",\n        \"message\": {\n            \"text\": \"Your order has been fulfilled!\",\n            \"timestamp\": int(time.time())\n        }\n    }\n}\nboto3.client(\"sns\").sns.publish(\n    TargetArn=os.environ[\"NOTIFICATIONS_TOPIC_ARN\"],\n    Message=json.dumps(message),\n)\n```\n\n### Sending notification requests through SQS\n\nUnlike SNS, when sending notifications to SQS queue, the Pusher Lambda Function event source can be configured to poll\nnotification requests from the SQS Queue, and it will periodically poll notification requests from the Queue using\nPolling Technique.\n\nThis will result in notifications requests to be processed in batches, which comes with many benefits:\n\n- **Fewer Lambda Invocations** - to not reach the Lambda Concurrency Limit.\n- **Concurrent Notifications** - as the pusher uses **`AsyncIO`**, it will be able to process batches of SQS Records\n  concurrently.\n- **Low cost** - thanks to SQS Batches and fewer Lambda Invocations.\n\nPusher can meet the same speed and performance of SNS if the SQS queue **`receive_wait_time_seconds`** is set to 20.\nthis will make the Lambda Service do **`Long Polling`** instead of **`Short Polling`**. In addition to that, Lambda\nservice will have a background worker that has five instances polling every 20 seconds, this will ensure that the lambda\nwill receive the notifications requests as soon as they arrive in the queue.\n\n\n\u003e **`AWS`**: the automatic scaling behavior of Lambda is designed to keep polling costs low when a queue is empty while\n\u003e simultaneously letting us scale up to high throughput when the queue is being used heavily. When an SQS event source\n\u003e mapping is initially created and enabled, or when messages first appear after a period with no traffic, then the\n\u003e Lambda service will begin polling the SQS queue using five parallel long-polling connections. The Lambda service\n\u003e monitors the number of inflight messages, and when it detects that this number is trending up, it will increase the\n\u003e polling frequency by 20 ReceiveMessage requests per minute and the function concurrency by 60 calls per minute.\n\u003e As long as the queue remains busy it will continue to scale until it hits the function concurrency limits.\n\n\n5 parallel connections, each will send 3 ReceiveMessage requests per minute which is 15 messages every minute. so 900\nevery  hour, 21600 every day and 648,000 every month.\n\nAWS gives you one million messages for free every month. After that it’s only $0.40 per million messages, so the cost\nis very low for consuming messages from SQS.\n\n\u003e Send to SQS when you have a large application with millions of users\n\n```python\nimport os\nimport json\nimport time\nimport boto3\n\nmessage = {\n    \"users\": [\"783304b1-2320-44db-8f58-09c3035a686b\", \"a280aa41-d99b-4e1c-b126-6f39720633cc\"],\n    \"data\": {\n        \"type\": \"notification\",\n        \"message\": {\n            \"text\": \"Your order has been fulfilled!\",\n            \"timestamp\": int(time.time())\n        }\n    }\n}\nboto3.client(\"sqs\").send_message(\n  QueueUrl=os.environ.get(\"NOTIFICATIONS_QUEUE_URL\"),\n  MessageBody=json.dumps(message),\n)\n```\n\n## Deploy\n\nBefore deploying the pusher, version control it on your github account and then call the [Terraform module](/infra) \nshipped with the Pusher to provision the Pusher Lambda Function, and the Terraform module to create CI/CD pipeline:\n\n```hcl\nmodule \"pusher\" {\n  source      = \"./infra\"\n  prefix      = local.prefix\n  common_tags = local.common_tags\n\n  agma_arn       = \"${aws_apigatewayv2_api._.execution_arn}/${aws_apigatewayv2_stage._.name}/POST/@connections\"\n  apigw_endpoint = \"https://live.kodhive.com/push\"\n  # Custom API Gateway Domain\n  connections_table = {\n    name = aws_dynamodb_table.connections.name\n    arn  = aws_dynamodb_table.connections.arn\n  }\n  notifications_topic_arn = aws_sns_topic.notifications.arn\n  notifications_queue_arn = aws_sqs_queue.notifications.arn\n}\n\nmodule \"pusher_ci\" {\n  source      = \"git::https://github.com/obytes/terraform-aws-lambda-ci.git//modules/ci\"\n  prefix      = \"${local.prefix}-pusher-ci\"\n  common_tags = var.common_tags\n\n  # Lambda\n  lambda                   = module.pusher.lambda\n  app_src_path             = \"src\"\n  packages_descriptor_path = \"src/requirements/lambda.txt\"\n\n  # Github\n  pre_release  = true\n  s3_artifacts = {\n    arn    = aws_s3_bucket.artifacts.arn\n    bucket = aws_s3_bucket.artifacts.bucket\n  }\n  github = {\n    owner          = \"obytes\"\n    webhook_secret = \"not-secret\"\n    connection_arn = \"arn:aws:codestar-connections:us-east-1:{ACCOUNT_ID}:connection/{CONNECTION_ID}\"\n  }\n  github_repository = {\n    name   = \"apigw-websocket-pusher\"\n    branch = \"main\"\n  }\n  # Notifications\n  ci_notifications_slack_channels = {\n    info  = \"ci-info\"\n    alert = \"ci-alert\"\n  }\n}\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobytes%2Fapigw-websocket-pusher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fobytes%2Fapigw-websocket-pusher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fobytes%2Fapigw-websocket-pusher/lists"}