Features | Pricing | Documentation | Contact | Blog | About

Now Available: Packet Sources

By Lee Harding | June 8, 2026 | 6 min read
Diagram showing server-initiated UDP packet delivery through a Proxylity Listener

Packet Sources are now generally available to all customers with an AWS Marketplace subscription. With a Packet Source, your application can send UDP packets to connected clients at any time — without waiting for those clients to send a request first. Push notifications, game state updates, over-the-air firmware delivery, async acknowledgements: they're all straightforward to build now, on top of the same Listeners you already use for inbound traffic.

We announced the feature in Select Availability back in April. This post covers what's changed since then and what you need to know to get started.

What Changed Since Select Availability

The design described in the original article — an SNS topic in your account bound to a Listener via a CloudFormation resource — is the same. But several things have been refined or added since April.

Message format

Each SNS message body must now wrap packets in a Messages array. This unlocks fan-out: a single SNS publish can deliver to multiple clients in one operation, which is useful for broadcasting game state or sensor commands to a group of peers.

{
  "Messages": [
    {
      "Data": "AQIDBA==",
      "Remote": { "Address": "203.0.113.42", "Port": 4567 }
    }
  ]
}

The Remote object uses the field name Address (not IP, as shown in the earlier article). The rest of the structure is unchanged.

EgressRegion — required message attribute

This is the most important operational detail. Proxylity subscribes one SQS delivery queue per supported AWS region to your SNS topic, each with a filter policy keyed on the EgressRegion SNS message attribute. You must set this attribute on every publish; without it the message matches no filter and is silently discarded — no packet is delivered.

Set EgressRegion to the AWS region where you want the packet to exit the network. For replies to inbound packets, use the IngressRegion value from the original packet JSON — this ensures the reply takes the same regional path the client's traffic entered, which is required for WireGuard session compatibility.

import boto3, json, base64

sns = boto3.client("sns")
payload = b"\x01\x02\x03\x04"

sns.publish(
    TopicArn=REPLY_TOPIC_ARN,
    MessageAttributes={
        "EgressRegion": {"DataType": "String", "StringValue": "us-east-1"}
    },
    Message=json.dumps({
        "Messages": [
            {
                "Data": base64.b64encode(payload).decode(),
                "Remote": {"Address": "203.0.113.42", "Port": 4567}
            }
        ]
    })
)

All formatters are supported

The select availability release only supported base64 encoding. The GA release supports all the same formatters available to Destinations: base64, hex, utf8, ascii, and coap. Specify the optional Formatter field in each message entry; it defaults to base64 when omitted.

WireGuard Support

Packet Sources work with both plain UDP Listeners and WireGuard Listeners. For WireGuard you set Remote.PeerKey to the base64-encoded public key of the target peer; the Gateway looks up the active WireGuard session for that key and encrypts the packet before delivery. If no session exists for the key, the packet is dropped.

If your WireGuard Listener has DecapsulatedDelivery enabled, sourced packets must also include an Inner block with inner IP/UDP addressing so the Gateway can re-encapsulate the payload correctly before sending it through the tunnel:

{
  "Messages": [
    {
      "Data": "AQIDBA==",
      "Remote": {
        "Address": "203.0.113.42",
        "Port": 51820,
        "PeerKey": "base64encodedWireGuardPublicKey=="
      },
      "Inner": {
        "SourceAddress": "10.0.0.1",
        "SourcePort": 53,
        "DestinationAddress": "10.0.0.2",
        "DestinationPort": 12345
      }
    }
  ]
}

Setting Up a Packet Source

The CloudFormation resource is Custom::ProxylityUdpGatewayPacketSource. It binds an SNS topic you own to a named Listener. Proxylity assumes the role you provide to subscribe its delivery queues to the topic; those subscriptions are removed automatically when you delete the resource.

The IAM policy for the role must be attached before CloudFormation attempts to create the Packet Source, so use DependsOn to enforce the ordering:

ReplyTopic:
  Type: AWS::SNS::Topic

ProxylitySubscribeToTopicPolicy:
  Type: AWS::IAM::Policy
  Properties:
    PolicyName: ProxylitySubscribeToTopicPolicy
    PolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Action:
            - sns:Subscribe
            - sns:Unsubscribe
            - sns:ListSubscriptionsByTopic
          Resource: !GetAtt ReplyTopic.TopicArn
    Roles:
      - !Ref RoleForProxylity

ReplySource:
  DependsOn:
    - ProxylitySubscribeToTopicPolicy
  Type: Custom::ProxylityUdpGatewayPacketSource
  Properties:
    ServiceToken: !FindInMap [ProxylityConfig, !Ref "AWS::Region", ServiceToken]
    ApiKey: !FindInMap [ProxylityConfig, Account, ApiKey]
    ListenerName: !Ref MyListener
    SnsTopicSource:
      TopicArn: !GetAtt ReplyTopic.TopicArn
      Role: !GetAtt RoleForProxylity.Arn

Example: CoAP Observe with a WireGuard Listener

The best place to start is the CoAP Time Service in the Proxylity examples repository. It deploys a fully working CoAP server on AWS — complete with RFC 7641 Observe (server-push subscriptions) and RFC 6690 Discovery — and is the canonical example of Packet Sources used in production.

The service responds to CoAP GET /time requests with the current UTC time. Clients can also subscribe with the Observe option to receive a push notification every minute without sending another request. That minute-cadence push is entirely driven by a Packet Source.

Architecture

All traffic travels through a WireGuard Listener with DecapsulatedDelivery enabled. Inbound CoAP packets are decoded by Proxylity's coap formatter and delivered as structured JSON to an Express Step Functions state machine, which handles routing, Observe registration, and per-request responses. Subscriptions are stored in a DynamoDB table with a 180-second TTL.

An EventBridge Scheduler fires a Lambda function every minute. It scans the subscription table and publishes one CoAP NON notification per active subscriber to the SNS reply topic. A Packet Source bound to that topic delivers each notification back to the subscriber through the same WireGuard Listener — with the coap formatter re-encoding the JSON back into a binary CoAP packet before it leaves the gateway.

The outbound envelope the Lambda publishes looks like this:

{
  "Messages": [
    {
      "Formatter": "coap",
      "Data": "{\"Type\":1,\"Code\":\"2.05\",\"MessageId\":4711,\"Token\":\"AQID\",\"Options\":[{\"Number\":6,\"Value\":\"AB0=\"},{\"Number\":12,\"Value\":\"\"},{\"Number\":14,\"Value\":\"PA==\"}],\"Payload\":\"MjAyNi0wNi0wOFQxMjowMDowMFo=\"}",
      "Remote": {
        "Address": "203.0.113.5",
        "Port": 51820,
        "PeerKey": "base64encodedWireGuardPublicKey=="
      },
      "Inner": {
        "SourceAddress": "10.10.10.21",
        "SourcePort": 5683,
        "DestinationAddress": "10.10.10.20",
        "DestinationPort": 5683
      }
    }
  ]
}

Two things are worth noting here. First, Formatter: "coap" applies on the outbound path: the Data field is a stringified JSON CoAP message that Proxylity re-encodes into binary before sending. Second, the Inner block is required because the Listener has DecapsulatedDelivery enabled — Proxylity uses it to re-encapsulate the payload into an inner IP/UDP frame before WireGuard encryption, so the client sees packets arriving with the expected inner tunnel addressing.

Observe sequence numbers without a counter

RFC 7641 requires that each notification carry a strictly increasing Observe sequence number (Option 6) within a 128-second comparison window. The Lambda uses a neat trick to satisfy this without maintaining persistent state: it derives the 24-bit sequence number from UnixEpoch mod 2²⁴. Since exactly 60 seconds elapses between sends, each value is always greater than the previous within the comparison window, and the approach survives Lambda cold starts and restarts without any counter storage.

Deploying the example

The example deploys with AWS SAM. Full instructions are in the repository README; the short version is:

wg genkey | tee client.key | wg pubkey > client.pub
sam build && sam deploy --guided \
  --parameter-overrides WireGuardClientPublicKey=$(cat client.pub)

Once deployed, a WireGuard tunnel configuration is generated from the stack outputs and you can test with any CoAP client. The README includes instructions for using coap-client from libcoap and for setting up the tunnel alongside other active WireGuard interfaces without conflicts.

Example: Raptor Object Transfer

The Raptor project uses a Packet Source for a different purpose: acknowledgements in a reliable UDP transfer protocol. A sender encodes a file with RaptorQ fountain codes and floods packets at a WireGuard Listener; Lambda functions reassemble blocks as they arrive. As each block is decoded, the block-completer Lambda publishes an acknowledgement to the SNS reply topic, which Proxylity delivers back to the encoder through the same Listener. The encoder stops retransmitting blocks that have been acknowledged, turning an unreliable UDP flood into a reliable transfer.

Raptor is a good reference for multi-Lambda architectures and for seeing Packet Sources used in a pipeline rather than a request/response pattern.

Billing and Availability

Packets delivered via a Packet Source are billed at the same per-packet rate as inbound packets, consistent with your subscription tier. There is no separate charge for the feature itself.

Packet Sources are available today to all customers with an AWS Marketplace subscription. No approval or waitlist — deploy the CloudFormation resource and start publishing.

Documentation

The full reference documentation is now live:

Ready to modernize your UDP backends?

Get started with Proxylity UDP Gateway today. No upfront costs – pay only for what you use.

Buy with AWS Try the Examples Explore Documentation