ExAws.SQS
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:
- Switches every operation from the legacy Query/XML protocol to the SQS JSON protocol
(ex-aws/ex_aws_sqs#34), which AWS recommends
going forward for lower latency and less client-side overhead. This also drops the
:saxy/:sweet_xmldependency entirely — one less thing to configure or upgrade. - Documents
send_message_batch/2with a runnable example (ex-aws/ex_aws_sqs#35). - Relaxes the
:hackneyversion constraint so it no longer collides with apps that have moved to hackney 4.x (ex-aws/ex_aws_sqs#36).hackneyis only used to run this library's own test suite — request execution always goes through whatever HTTP adapter your app configures forex_aws. - Adds the message-move-task operations (
start_message_move_task/2,cancel_message_move_task/1,list_message_move_tasks/2) andListQueues/ListDeadLetterSourceQueuespagination options, none of which existed yet when upstream went quiet. - Refreshes
mix.exs/CI to current Elixir/OTP versions.
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.
Copyright and License
The MIT License (MIT)
Copyright (c) 2014 CargoSense, Inc. Copyright (c) 2026 BeamLab EU
See LICENSE for the full text.