Skip to content
DnsLister Forum

Where domain hunters compare notes

Lambda Integration

Proxylity's Lambda destination is where the most interesting compute happens. It's the path to take when you need to parse binary packets, validate them, transform them, and optionally send a reply back to the client. But there are several ways to invoke Lambda from Proxylity, and you can pick the one that matches your use case best.

This post walks through the three Lambda invocation modes, when to use each, and how they interact with batching and the response contract.

Synchronous request/response

The standard integration uses InvokeFunction (RequestResponse type). Proxylity batches packets, invokes your function, waits for the response, and sends any reply packets back to clients.

Destinations: - DestinationArn: !GetAtt MyFunction.Arn Role: Arn: !GetAtt DestinationRole.Arn Batching: Count: 100 TimeoutInSeconds: 0.5 

Your function receives:

{ "Messages": [ { "Tag": "abc123", "Remote": { "IpAddress": "1.2.3.4", "Port": 5678 }, "Local": { "Domain": "ingress-1", "Port": 15678 }, "Data": "<base64-encoded payload>", "ReceivedAt": "2026-01-01T00:00:00.000Z" } ] } 

And returns:

{ "Replies": [ { "Tag": "abc123", "Data": "<base64-encoded response>" } ] } 

The Tag is the key. It's an opaque identifier that ties a reply to the original sender; echo it back and Proxylity routes your response to the correct client's IP:port. You never deal with socket addressing.

If your code has nothing to say (no response), simply return { "Replies": [] }. Don't return nulls or omit the field.

Use this model for any protocol with request/response semantics: DNS resolution, RADIUS auth, CoAP GET/POST, game actions that need immediate acknowledgment.

Response streaming: replies before the function finishes

Set Arguments.UseResponseStreaming: "true" and Proxylity invokes your function using the InvokeWithResponseStream API. Instead of buffering all replies until the function completes, reply packets are sent to the remote client as soon as your function writes them to the response stream.

Destinations: - DestinationArn: !GetAtt StreamingFunction.Arn Role: Arn: !GetAtt DestinationRole.Arn Arguments: UseResponseStreaming: "true" Batching: Count: 1 TimeoutInSeconds: 0.05 

This gives you two things:

  • Reduced time to first packet. The client gets an ack or initial response while your function is still working on the rest. Valuable for protocols that need rapid acknowledgment before doing heavier processing.
  • Continuous packet delivery. Your function can keep sending reply packets for its entire execution duration (up to the configured timeout). This enables sustained bidirectional communication patterns — send a stream of updates, progress reports, or chunked data back to the client over time.

Your Lambda must be implemented as a streaming function (see the AWS docs for the latest supported runtimes and SDKs). As you write response packet objects to the stream, Proxylity transmits them immediately.

Use this model for long-running operations that need to send incremental results, protocols with sustained server-to-client data delivery (e.g. audio streaming), or anything where time-to-first-byte matters more than total throughput. CoAP Block and Observe are other use cases, thought only for relative short-running observe periods (I'll post more about CoAP Observe and PacketSources for long-running observes).

Note: Streaming bandwidth is uncapped for the first 6 MB of response data; beyond that, AWS caps at 2 MB/s.

Async invocation: fire-and-forget compute

Set Arguments.UseAsyncInvoke: "true" and Proxylity uses the Event invocation type. The Gateway delivers the packet batch and returns immediately (HTTP 202 Accepted) without waiting for the function to complete. No reply packets are ever sent back to the client, regardless of what the function returns.

Destinations: - DestinationArn: !GetAtt WorkflowTrigger.Arn Role: Arn: !GetAtt DestinationRole.Arn Arguments: UseAsyncInvoke: "true" 

Key characteristics:

  • The function's return value is discarded entirely.
  • AWS may automatically retry failed invocations, so make your function idempotent, or configure a dead-letter queue.
  • UseAsyncInvoke and UseResponseStreaming are mutually exclusive. Set at most one.

Lambda Durable Functions use a checkpoint-and-replay mechanism to execute for up to one year, automatically resuming after failures. With async invocation, the Gateway delivers packets and moves on while the durable execution continues independently. This is ideal for long-lived workflows triggered by UDP input like device provisioning sequences, multi-step (e.g. human in the loop) protocol negotiations, or anything that outlives a single invocation.

It's also appropriate when the function's role is just to dispatch. Hand off requests to Step Functions, write to a queue, or trigger downstream workflows, and no need to reply.

Use this for Telemetry ingestion where you want Lambda-level flexibility but don't need responses. Workflow triggers. Durable function entry points. Anything where "received, processing" is implicit and the client doesn't wait.

Step Functions: an alternative to Lambda

Step Functions is worth mentioning alongside Lambda because it serves a similar role. Step Functions EXPRESS state machines receive the same { Messages: [...] } payload and return the same { Replies: [...] } format. Proxylity auto-detects the state machine type via states:DescribeStateMachine.

The advantages over Lambda:

  • No cold starts (~50ms execution overhead instead), and more consistent tail latency.
  • Quicker creation and updates.
  • STANDARD retains state between steps, like Lambda durable functions.
  • JSONata supports makes packet processing and reply construction concise.
  • Good for text processing, so use with utf-8 formatter

EXPRESS executions max out at 5 minutes. STANDARD state machines are invoked asynchronously and cannot send replies (same semantics as UseAsyncInvoke on Lambda).

Use this for multi-step processing where you'd otherwise be writing light parsing and SDK call boilerplate in Lambda. SYSLOG real-time conditional alerting. Anything with a sequence of SDK/API calls.

Batching is your cost/latency dial

Regardless of invocation mode, Batching.Count and Batching.TimeoutInSeconds control how packets are grouped before invoking your function:

  • Count: maximum packets per batch (keep the Lambda limits in mind).
  • TimeoutInSeconds: maximum wait time before invoking with whatever has accumulated.

Whichever threshold is hit first triggers the invocation. This is your primary cost/latency lever:

Use case Count Timeout Tradeoff
Real-time protocol (DNS, game) 1-10 0.05-0.1 Low latency, more invocations
Balanced throughput 10-100 0.5-1.0 Good latency, efficient batching
Bulk ingestion 100-1000 5-10 Maximum efficiency, higher latency (consider using async invoke, perhaps Kinesis instead)

For streaming mode, you'll may want a low count and tight timeout so packets arrive at the function quickly and replies flow back without buffering delay.

Formatter options

Each destination can specify how packet Data is encoded when it reaches your function:

  • base64 (default): raw binary as base64. Decode in your handler.
  • utf8 / ascii: payload delivered as a plain string. No decoding needed.
  • hex: lowercase hex string.
  • coap: binary CoAP packet auto-parsed to a JSON object (headers, options, payload separated for you).

Formatter is set per-destination, so the same listener can route to one function with "Formatter": "coap" and another with "Formatter": "base64".

Important: reply Data always matches the inbound formatter, so if you're receiving utf-8, reply with it as well. And hex for hex, etc.

Tenant isolation

Lambda functions with Tenant Isolation mode enabled can receive a tenant identifier with each invocation. Proxylity supports:

  • Static: Arguments.TenantId: "my-tenant" — all packets use the same tenant ID.
  • Dynamic: Arguments.TenantIdExpression: "[0:4]" with optional TenantIdFormatter — extracts tenant ID from packet bytes using BREX.

When using dynamic tenant extraction, batches are automatically segmented by tenant ID before delivery. Each invocation contains packets for only one tenant.

Pick your model

Question Answer Mode
Does the client need a reply? Yes, immediately Request/Response (default)
Does the client need a reply? Yes, but function runs long Response Streaming
Does the client need a reply? No Async Invocation
Is this a long-lived workflow? Yes, up to 1 year Async + Durable Functions
Multi-step bounded protocol? Yes, < 5 min Step Functions Express
Need server-push later (not in response to a packet)? Yes PacketSource (next time)

Is that enough flexibility? We hope so!

Source: r/proxylity · by /u/mlhpdx

Leave a Reply

Your email address will not be published. Required fields are marked *