Run a .NET SQS Worker Environment on Elastic Beanstalk¶
This recipe shows how to process asynchronous workloads with a .NET application in an Elastic Beanstalk worker environment. The worker tier's built-in SQS daemon polls the queue and delivers each message as an HTTP POST to your application. Your .NET code only needs to handle HTTP requests — it never calls the SQS API directly.
Prerequisites¶
- Familiarity with Elastic Beanstalk environment tiers (web server vs worker).
- AWS CLI, EB CLI, and .NET 8 SDK installed locally.
- An Elastic Beanstalk application already initialized for your repository.
- IAM permissions for Elastic Beanstalk, Amazon SQS, and CloudWatch Logs.
What You'll Build¶
- A main SQS queue for background jobs and a dead-letter queue (DLQ) for poison messages.
- An Elastic Beanstalk worker environment created with
--tier worker. - A .NET web application that receives SQS messages as HTTP POST requests from the worker daemon and returns
200 OKon success.
flowchart LR
A[Producer / Web Environment] -->|SendMessage| B[Amazon SQS Work Queue]
B --> C[EB Worker Daemon sqsd]
C -->|HTTP POST /| D[.NET Web Application]
D -->|200 OK| C
C -->|DeleteMessage| B
D -->|non-200| C
C -->|retry after ErrorVisibilityTimeout| B
B -. maxReceiveCount exceeded .-> E[Dead-Letter Queue] Steps¶
-
Create the DLQ first, then create the work queue with a redrive policy.
aws sqs create-queue \ --queue-name "${APP_NAME}-jobs-dlq" \ --region "$REGION" DLQ_ARN=$(aws sqs get-queue-attributes \ --queue-url "https://sqs.${REGION}.amazonaws.com/<account-id>/${APP_NAME}-jobs-dlq" \ --attribute-names QueueArn \ --query 'Attributes.QueueArn' \ --output text \ --region "$REGION") aws sqs create-queue \ --queue-name "${APP_NAME}-jobs" \ --attributes "VisibilityTimeout=120,ReceiveMessageWaitTimeSeconds=20,RedrivePolicy={\"deadLetterTargetArn\":\"${DLQ_ARN}\",\"maxReceiveCount\":\"5\"}" \ --region "$REGION" -
Create a minimal .NET web project. The worker daemon handles all SQS interaction, so no SQS SDK is needed in the application itself.
-
Implement the HTTP handler that the worker daemon calls. The daemon POSTs each SQS message body to the configured
HttpPath(default/). Return200 OKafter successful processing — the daemon deletes the message. Return any non-200 status and the daemon returns the message to the queue for retry.var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.MapPost("/", async (HttpContext context, ILogger<Program> logger) => { using var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); var sqsMessageId = context.Request.Headers["X-Aws-Sqsd-Msgid"].FirstOrDefault() ?? "unknown"; var receiveCount = context.Request.Headers["X-Aws-Sqsd-Receive-Count"].FirstOrDefault() ?? "1"; logger.LogInformation("Processing SQS message {MessageId} receive-count={ReceiveCount}", sqsMessageId, receiveCount); using var document = System.Text.Json.JsonDocument.Parse(body); var jobType = document.RootElement.GetProperty("jobType").GetString(); var jobId = document.RootElement.GetProperty("jobId").GetString(); if (string.IsNullOrWhiteSpace(jobType) || string.IsNullOrWhiteSpace(jobId)) { logger.LogError("Invalid message schema — missing jobType or jobId"); return Results.BadRequest("jobType and jobId are required"); } // Replace with your actual business logic await Task.Delay(TimeSpan.FromSeconds(2)); logger.LogInformation("Completed job {JobId} type={JobType}", jobId, jobType); return Results.Ok(); }); app.Run();Tip
The worker daemon injects SQS metadata as HTTP headers. Use
X-Aws-Sqsd-Msgidfor the SQS message ID,X-Aws-Sqsd-Receive-Countfor retry tracking, andX-Aws-Sqsd-Queuefor the source queue URL. -
Add a
Procfileso Elastic Beanstalk starts the application process. -
Configure worker daemon settings with
.ebextensions/worker.config.option_settings: aws:elasticbeanstalk:sqsd: WorkerQueueURL: "https://sqs.$REGION.amazonaws.com/<account-id>/${APP_NAME}-jobs" HttpPath: "/" MimeType: "application/json" VisibilityTimeout: 120 ErrorVisibilityTimeout: 30 InactivityTimeout: 299 MaxRetries: 5VisibilityTimeoutmust exceed worst-case processing time so in-progress messages are not redelivered.ErrorVisibilityTimeoutcontrols how quickly a failed message becomes visible again for retry.MaxRetriesworks alongside the queue'smaxReceiveCountredrive policy.InactivityTimeoutsets the HTTP connection timeout between the daemon and your application.
-
Create the worker environment and deploy.
eb create "$ENV_NAME" \ --tier worker \ --region "$REGION" \ --single \ --instance_type t3.small eb deploy "$ENV_NAME" --stagedWarning
Do not configure your application to poll SQS directly when running on the worker tier. The worker daemon already handles polling, message deletion, and retry. Running both consumers against the same queue causes duplicate processing and unpredictable message deletion.
-
Send a test message that matches the handler contract.
-
Monitor queue depth, in-flight work, and DLQ movement.
aws sqs get-queue-attributes \ --queue-url "https://sqs.${REGION}.amazonaws.com/<account-id>/${APP_NAME}-jobs" \ --attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible \ --region "$REGION" aws sqs get-queue-attributes \ --queue-url "https://sqs.${REGION}.amazonaws.com/<account-id>/${APP_NAME}-jobs-dlq" \ --attribute-names ApproximateNumberOfMessages \ --region "$REGION"- Make handlers idempotent because SQS provides at-least-once delivery.
- If work often exceeds 120 seconds, raise both queue
VisibilityTimeoutand daemonVisibilityTimeouttogether.
Verification¶
Use these checks after deployment:
Expected outcomes:
- The worker environment is healthy and
Ready. - Test messages leave the main queue after successful processing.
- Application logs show
Processing SQS messageandCompleted jobentries. - Failed messages reappear after the
ErrorVisibilityTimeoutand move to the DLQ after exceedingmaxReceiveCount.
Failure-mode guidance:
- If the same message returns too quickly, the
VisibilityTimeoutis shorter than the processing time. - If messages disappear without business effect, your handler returns
200 OKbefore the downstream operation completes. - If the DLQ grows steadily, inspect schema mismatches, downstream failures, and idempotency gaps.
- If queue depth rises while the environment looks healthy, your arrival rate exceeds single-instance throughput — scale up or add instances.