Skip to main content

Running in production

This guide runs an SQS service on AWS: where its credentials and region come from, the timeouts it runs with, and the IAM permissions it needs.

Credentials and region from the environment​

SqsBroker::new() takes nothing from the code. The AWS SDK's default chain finds the region and the credentials when the service starts: environment variables, the shared ~/.aws files, SSO, and the role of the container or the instance. A service that runs under an IAM role on ECS, EKS, Lambda or EC2 needs no configuration at all.

The SDK reads credentials when it signs its first request. That request is the GetQueueUrl of the first subscription, at startup, so a service with wrong credentials stops before it handles a message.

A configuration of your own​

src/main.rs
/// The AWS configuration of the service: a pinned behaviour version, its region, the
/// credentials of the `orders-service` profile, read when the first request is signed, and a
/// request timeout above the 20 seconds a long poll waits.
fn aws_config() -> SdkConfig {
let credentials = ProfileFileCredentialsProvider::builder()
.profile_name("orders-service")
.build();
SdkConfig::builder()
.behavior_version(BehaviorVersion::v2026_01_12())
.region(Region::new("eu-west-1"))
.credentials_provider(SharedCredentialsProvider::new(credentials))
.timeout_config(
TimeoutConfig::builder()
.operation_attempt_timeout(Duration::from_secs(25))
.build(),
)
.build()
}

SqsBroker::from_config takes an SdkConfig the service builds. aws_config builds one from the environment with an async loader, while the app function is synchronous. So the example builds it with SdkConfig::builder(), which does no I/O. The profile provider reads the orders-service profile on the first request; a profile with a role_arn assumes that role. The pinned behaviour version keeps the SDK's defaults from changing under the service when the SDK is updated. The configuration also carries the request timeout described below: the broker keeps every setting of the configuration except an endpoint or a region named on the broker itself. The example needs two more dependencies:

Cargo.toml
[dependencies]
aws-config = "1"
aws-sdk-sqs = "1"

The service​

src/main.rs
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "orders")]
struct Order {
id: u64,
amount_cents: u64,
}

// The queue belongs to the infrastructure, so the descriptor does not create it. It names its
// visibility, so the subscription does not read the queue's own.
#[subscriber(SqsQueue::new("orders").visibility(Duration::from_secs(120)))]
async fn take_order(order: &Order) -> HandlerOutcome {
if order.amount_cents == 0 {
tracing::warn!(order.id, "order without an amount dropped");
return HandlerOutcome::drop();
}
tracing::info!(order.id, order.amount_cents, "order taken");
HandlerOutcome::ack()
}

#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
SqsBroker::from_config(aws_config()),
|b| {
b.include(take_order);
},
)
}

The orders queue belongs to the infrastructure: Terraform, CloudFormation or the console create it, with its redrive policy and encryption. The descriptor leaves out create_if_missing, so a missing queue stops the service at startup instead of creating an empty one. It names its visibility, so the subscription does not read the queue's own timeout.

endpoint and region point the broker at another endpoint, a VPC endpoint or a local stack. On a broker built with from_config they replace the configuration's own endpoint_url and region. The queue URLs SQS returns are rebased onto the endpoint.

Timeouts and transport​

SqsBroker::new() gives each SQS request 25 seconds per attempt, above the 20 seconds a long poll may wait. A configuration of your own sets this with timeout_config, as the example does; an attempt timeout under 20 seconds would end every long poll. The SDK retries a failed request with its own retry policy. Requests go over the SDK's default HTTPS client. A handler on a dedicated thread (threads(n)) sends through SDK clients of its own, built from the same configuration and sharing its credentials.

IAM permissions​

The service above needs these permissions on its queue:

policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sqs:GetQueueUrl",
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:ChangeMessageVisibility"
],
"Resource": "arn:aws:sqs:eu-west-1:123456789012:orders"
}
]
}

Each feature adds its own:

FeaturePermissions
a subscription that names no visibilitysqs:GetQueueAttributes on the queue
max_attempts and dead_lettersqs:SetQueueAttributes on the queue, sqs:GetQueueAttributes on the dead-letter queue
create_if_missingsqs:CreateQueue
a publish to a queuesqs:GetQueueUrl and sqs:SendMessage on the queue
a publish to a topicsns:CreateTopic and sns:Publish on the topic
subscribe_queue_to_topicsns:CreateTopic and sns:Subscribe on the topic, sqs:GetQueueUrl, sqs:GetQueueAttributes and sqs:SetQueueAttributes on the queue

What the AsyncAPI document shows​

With the asyncapi feature, the service's AsyncAPI document names one server: the host of the broker's endpoint, or else the regional host of its region, such as sqs.eu-west-1.amazonaws.com. The document never holds a credential or a queue's ARN, so it can be published.

The tests in the example file start the same app in process; the in-process SQS takes the region from the configuration and signs nothing. Testing an SQS service covers the harness.