ExAws.SQS

Hex.pmHex DocsLicense

Service module for ex_aws.

This is a modernized fork of the archived ex-aws/ex_aws_sqs (last released in Jan 2023). It exists because the upstream project stopped receiving updates while its open issues stayed unresolved. This fork:

Installation

Published to Hex as beamlab_ex_aws_sqs because the original ex_aws_sqs package name is already taken: https://hex.pm/packages/beamlab_ex_aws_sqs. The compiled OTP application is still named :ex_aws_sqs, so use Mix's :hex dependency option to fetch this package under its normal name — this keeps the app a drop-in replacement for packages (like broadway_sqs) that declare a dependency on :ex_aws_sqs directly:

def deps do
[
{:ex_aws, "~> 2.7"},
{:ex_aws_sqs, "~> 5.0", hex: :beamlab_ex_aws_sqs},
# No Jason needed — Elixir >= 1.18 has built-in JSON support.
# Use `config :ex_aws, json_codec: JSON` (or keep Jason if preferred).
{:hackney, "~> 4.0"} # or any HTTP client ex_aws supports (only for tests here)
]
end

Or track main directly:

{:ex_aws_sqs, github: "BeamLabEU/beamlab_ex_aws_sqs"}

Note: The public API (module ExAws.SQS) and configuration (config :ex_aws, :sqs, ...) remain the same as the original. Only the Hex package name differs from the app name — see #1 for background.

Migrating from ex-aws/ex_aws_sqs

The public function names and options are unchanged — swapping the dependency source is enough to compile. What does change is the shape of a successful response, because there's no more XML-to-map parsing layer standing between you and AWS:

# before (ex-aws/ex_aws_sqs, XML/Query protocol)
{:ok, %{body: %{messages: [%{message_id: id, body: body} | _]}}} =
ExAws.SQS.receive_message(queue_url) |> ExAws.request()
# after (this fork, JSON protocol)
{:ok, %{"Messages" => [%{"MessageId" => id, "Body" => body} | _]}} =
ExAws.SQS.receive_message(queue_url) |> ExAws.request()

In short: response bodies are now the raw JSON payload AWS returns, decoded by your configured :json_codec (Elixir's built-in JSON module since 1.18, or e.g. Jason) — keyed exactly as the AWS API Reference documents for each action, with no snake_case/atom conversion. A couple of operations use unusual casing straight from AWS (e.g. list_dead_letter_source_queues/1 returns a lowercase "queueUrls" key) — that's an AWS quirk carried through as-is, not a bug here.

Binary message attribute values (if any) will appear base64-encoded in the raw responses.

You'll also want to drop :saxy and :sweet_xml from your deps if you added them for the old parser, and can drop any config :ex_aws_sqs, parser: ... config — there's no parser to select anymore.

One behavioral note: under the JSON protocol the QueueUrl travels inside the request body, and the HTTP request always goes to the endpoint from your config :ex_aws, :sqs (host/region) — the host embedded in the QueueUrl is not used to route the request (the 3.x Query-protocol implementation derived the request path from it). This matches the official AWS SDKs: use queue URLs whose region and account match your ex_aws config, otherwise AWS rejects the call server-side.

send_message_batch/2

Each entry is a keyword list (or map) with at least :id and :message_body:

ExAws.SQS.send_message_batch(queue_url, [
[id: "a1", message_body: "payload1"],
[id: "a2", message_body: "payload2", delay_seconds: 10]
])
|> ExAws.request()
# {:ok, %{
# "Successful" => [%{"Id" => "a1", "MessageId" => "...", "MD5OfMessageBody" => "..."}, ...],
# "Failed" => []
# }}

:id only needs to be unique within the batch — it's how you match each entry to its result in "Successful"/"Failed".

Entries can be provided as keyword lists or maps.

Message attributes

When sending messages with :message_attributes, use maps (or a list of maps) with :name, :data_type, and :value. Supported data types are :string, :number, and :binary.

For binary attributes, pass the raw binary as :value. The library automatically base64-encodes it to satisfy the JSON protocol on the wire.

On receive, any binary message attributes in the response will contain base64-encoded strings under "BinaryValue" (this is the raw form returned by AWS under the JSON protocol).

Example:

ExAws.SQS.send_message(queue_url, "body", message_attributes: [
%{name: "trace", data_type: :binary, value: <<1, 2, 3>>}
])

Message system attributes (X-Ray tracing)

send_message/3 and send_message_batch/2 entries also accept :message_system_attributes (same shape as :message_attributes). AWS currently supports only one system attribute, :aws_trace_header, which carries an X-Ray trace header so distributed tracing context survives the trip through the queue:

ExAws.SQS.send_message(queue_url, "body",
message_system_attributes: [
%{name: :aws_trace_header, data_type: :string, value: "Root=1-67891233-abcdef012345678912345678"}
]
)

Message move tasks (DLQ redrive)

These operations were added after the original library went quiet:

# Start moving messages from a DLQ back to the source (or another) queue
{:ok, %{"TaskHandle" => handle}} =
ExAws.SQS.start_message_move_task(dlq_arn) |> ExAws.request()
# Or with options
ExAws.SQS.start_message_move_task(dlq_arn,
destination_arn: target_arn,
max_number_of_messages_per_second: 100
)
# List recent tasks
ExAws.SQS.list_message_move_tasks(dlq_arn, max_results: 5) |> ExAws.request()
# Cancel if needed
ExAws.SQS.cancel_message_move_task(handle) |> ExAws.request()

See the AWS docs linked from each function for details and limits.

Streaming paginated lists

list_queues/1 and list_dead_letter_source_queues/2 paginate via NextToken. The stream_* helpers do the token plumbing for you and emit items lazily:

ExAws.SQS.stream_queues(queue_name_prefix: "prod-") |> Enum.to_list()
ExAws.SQS.stream_dead_letter_source_queues(dlq_url) |> Enum.take(50)

Pages are fetched with ExAws.request/2 and the stream raises a RuntimeError if a page request fails. An optional last argument is passed through as ExAws.request/2 config overrides (e.g. region: "eu-west-1"). list_message_move_tasks/2 has no stream counterpart — AWS returns a bounded list of recent tasks there and supports no pagination.

The MIT License (MIT)

Copyright (c) 2014 CargoSense, Inc. Copyright (c) 2026 BeamLab EU

See LICENSE for the full text.