Build a Retry-Aware SQS Order Processor

Build a JavaScript SQS worker that retries orders and isolates poison messages.

Introduction

30 Second Summary

An order can look finished while it is only hidden from view. Without a clear record of success, the same work can return later.

In this project, you will build a terminal-based JavaScript order processor with an independent producer and worker using Amazon Simple Queue Service (Amazon SQS). You will watch an undeleted order return before a corrected worker deletes successful messages.

What You'll Build

Picture sending an order in one terminal while another terminal exposes its full journey from first receive to retry to successful acknowledgement.

By the end of this project, you'll have:

  • A live order submission flow that prints a unique order ID after Amazon SQS accepts the JSON message.
  • Clear retry evidence when the same undeleted order returns after its visibility timeout.
  • A continuous long-polling worker that prints each processed order before confirming its deletion.
  • Secret Mission: Configure a dead-letter queue that quarantines a deliberately failing order after repeated receives.

Are there any prerequisites?

You need a personal AWS account whose current identity can create and manage SQS queues.

The project assumes basic JavaScript and AWS familiarity plus installed copies of Node.js, Visual Studio Code, and AWS Command Line Interface (AWS CLI).

Before We Start

Before any setup begins, commit to building independent JavaScript producer and worker processes that exchange orders through Amazon SQS. Your proof comes from watching an undeleted order return after its visibility timeout before the worker deletes successful messages with their receipt handles.

Set Up Your Local SQS Project

Your producer and worker will soon send real requests to Amazon Simple Queue Service (Amazon SQS). A missing local tool would stop the demo before SQS receives an order.

The Node.js runtime must be available. The AWS Command Line Interface (AWS CLI) must also have a valid AWS identity plus a configured Region.

You will finish by opening the project in Visual Studio Code. The package file pins the AWS SDK for JavaScript v3 dependency before npm installs it.

In this step, get ready to:
  • Verify the local Node.js runtime plus the AWS CLI version.
  • Confirm the AWS identity plus the configured Region.
  • Create the local project structure plus install its dependency.
Verify your local tools

Node.js runs the JavaScript files you build later. The AWS CLI provides an independent check that your machine can communicate with AWS.

  • Print the installed Node.js and AWS CLI versions by running these commands:
node --version
aws --version

What do these checks prove?

  • The node --version command confirms that your JavaScript runtime is available.
  • The aws --version command confirms that the AWS CLI is available.
  • AWS CLI version 2.32.0 or higher supports the browser-based sign-in path used in this step.
  • Select the tab that matches your terminal output.

✔️ I see the required version

You have cleared the first setup hurdle. Node.js is available and the AWS CLI is version 2.32.0 or higher.

ⓧ I see an older version

The browser-based sign-in command requires AWS CLI version 2.32.0 or higher. Updating now keeps the authentication path available if your current session has expired.

  • Follow the macOS instructions in the official AWS CLI installation guide.
  • Repeat the AWS CLI version check from above after the installer finishes.
  • Continue when the printed version is 2.32.0 or higher.

ⓧ A command is missing

A missing command means its tool is unavailable to your terminal. Install only the tool whose check failed.

Still missing a local tool?

Close your current terminal after installation. Start a fresh terminal so it loads the updated command path.

Still stuck? Help me diagnose why Node.js or the AWS CLI is unavailable in my terminal.

Confirm your AWS identity and Region

The SDK uses the same local AWS session as the CLI. An identity check confirms which account context receives your future SQS requests.

  • Ask AWS to return the identity behind your current session by running:
aws sts get-caller-identity

What does this identity check prove?

The command returns the IAM user or role represented by your current credentials. A successful response proves that the CLI has a valid AWS session.

  • Choose the tab that matches the identity check.

✔️ AWS returns my identity

Your AWS session is live. The returned identity is the account context that your local SQS scripts use.

ⓧ The identity check fails

Browser sign-in can feel sensitive because it authenticates your local terminal. This flow uses your existing AWS Management Console credentials without placing them in the project.

  • Start the browser-based AWS authentication flow by running:
aws login

What happens during sign-in?

The AWS CLI opens a browser authentication flow. Complete that flow with the AWS account you intend to use for this project.

The resulting session can refresh its credentials for up to 12 hours. Your JavaScript code can then use the local session without embedding credentials.

  • Complete the browser flow.
  • Return to your terminal after the browser confirms authentication.
  • Repeat the identity check shown above.

You should now receive account identity details from AWS.

Identity check still failing?

Confirm that the browser flow finished under the intended AWS account. Repeat the browser sign-in if the flow was cancelled or timed out.

Need help? Help me fix my AWS CLI session so the identity check succeeds.

Amazon SQS needs an AWS Region for every request. The SDK can read that Region from the shared local configuration.

  • Read the Region configured for your active AWS profile by running:
aws configure get region

Why check the Region?

The AWS SDK for JavaScript does not choose a Region by default. A configured Region gives every project script one consistent destination.

  • Choose the tab that matches the Region output.

✔️ I see a Region

Your local AWS configuration already has a Region. Keep the printed value for the rest of the project.

ⓧ The command returns no Region

An empty result means the active profile has no configured Region. This project uses us-west-2 as the default when one is missing.

  • Set the default Region and read it back by running:
aws configure set default.region us-west-2
aws configure get region

What does this configuration change?

The first command writes us-west-2 to the default AWS profile. The second command reads the saved value back as confirmation.

You should now see us-west-2 in the terminal.

Create the project and install dependencies

The producer and worker need one shared project folder. A single package file also keeps the SDK version consistent across every script.

  • Press Cmd+Space to open Spotlight on macOS.
  • Type Terminal and press Return.
  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The cd command changes the terminal's current folder. Starting from your Desktop makes the finished project easy to find.

  • Create the project structure and display its first folder by running:
mkdir sqs-order-processor
cd sqs-order-processor
mkdir src
ls

What does this project setup create?

  • The first mkdir command creates the sqs-order-processor folder on your Desktop.
  • The cd command moves the terminal into that new folder.
  • The second mkdir command creates the src folder for your JavaScript files.
  • The ls command lists the current folder contents.

You should see src in the terminal output. That visible folder confirms the project structure exists.

Project folder command failed?

A folder with the same name may already exist on your Desktop. Rename or remove the older folder before repeating the setup commands.

Need help? Help me fix the local folder setup for this project.

  • Leave the Terminal window open.
  • Press Cmd+Space to open Spotlight again.
  • Type Visual Studio Code and press Return.
  • Click File in the top menu.
  • Select Open Folder....
  • Select the sqs-order-processor folder on your Desktop.
  • Click Open.
  • Select Yes, I trust the authors if the Workspace Trust dialog appears.

You should see sqs-order-processor at the top of the Explorer sidebar. The src folder should appear underneath it.

  • Click the file-shaped control with a plus sign beside sqs-order-processor in the Explorer sidebar.
  • Enter package.json and press Return.

The new package.json file opens in the editor. This file defines the project format plus its dependency.

  • Fill package.json by pasting this configuration:
{
  "name": "sqs-order-processor",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "@aws-sdk/client-sqs": "3.1146.0"
  }
}

What does this package file configure?

  • The name field identifies the local project.
  • The private field prevents accidental publication as an npm package.
  • The type value loads the project's JavaScript files as ECMAScript modules.
  • The dependency pins @aws-sdk/client-sqs to version 3.1146.0.
  • Press Cmd+S on macOS or Ctrl+S on Windows to save package.json.
  • Check the Explorer sidebar for the two project entries.

You should see package.json beside the src folder.

Seeing a JSON warning?

Check every comma plus double quote against the reference below. JSON rejects missing commas and trailing commas.

Still stuck? Help me compare my package.json with the required project configuration.

✔️ Awesome, I've got everything!

Great. Confirm that package.json is saved beside the src folder.

ⓧ I'd like to double check the full code

Compare your saved file with this complete project configuration.

{
  "name": "sqs-order-processor",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "@aws-sdk/client-sqs": "3.1146.0"
  }
}

How to compare this file

Check each field in order. Confirm that the dependency name plus pinned version match exactly.

Before the final check, what do you expect each command to prove about your local setup?

  • Switch back to the Terminal window from earlier.
  • Verify the runtime, AWS session, Region, identity, and dependency installation by running:
node --version
aws --version
aws configure get region
aws sts get-caller-identity
npm install

What does the final check confirm?

  • The first command confirms that Node.js is available.
  • The second command prints the installed AWS CLI version.
  • The third command prints the configured AWS Region.
  • The fourth command returns the AWS account identity behind your current session.
  • The final command installs the dependency declared in package.json into the local project.

You should see the Node.js version plus the AWS CLI version first. Next, you should see a Region plus AWS account identity details.

The final output confirms that npm completed the dependency installation.

Dependency installation failed?

Confirm that the terminal is still inside the sqs-order-processor folder. The install command needs to find package.json in that folder.

A network restriction can also stop npm from downloading the SDK package. Help me diagnose why npm cannot install this project's SQS dependency.

That is the setup foundation in place. Next, you will create the SQS queue and submit its first order.

Queue Your First Order

Your Node.js project has its dependency installed. Your verified AWS session can now make requests in the configured Region.

A worker has nothing to process until an independent producer places a real order onto Amazon Simple Queue Service (Amazon SQS). This step creates the queue that holds the order.

In this step, get ready to:
  • Create the shared modules that connect each script to the same queue.
  • Create a Standard queue named nextwork-orders.
  • Send a serialized JSON order from an independent producer.
Create the shared queue modules

Each script needs the same queue name. The AWS SDK for JavaScript v3 also needs one reusable client that reads your local AWS configuration.

  • Use the file-creation control in the VS Code file list to create src/config.js inside the existing src folder.
  • Paste the shared queue name into src/config.js:
export const ORDER_QUEUE_NAME = "nextwork-orders";

What does this configuration do?

The ORDER_QUEUE_NAME constant keeps every script pointed at nextwork-orders. One shared value prevents queue-name mismatches.

  • Save src/config.js.
  • Confirm that config.js appears directly beneath src in the file list.

The client sends each command to Amazon SQS. An empty configuration object lets it use the Region from your shared configuration plus the credentials from your verified session.

  • Use the file-creation control in the VS Code file list to create src/client.js inside the existing src folder.
  • Paste the shared client into src/client.js:
import { SQSClient } from "@aws-sdk/client-sqs";

export const sqsClient = new SQSClient({});

What does this client do?

The SQSClient import provides the SDK client. The exported sqsClient gives every script one reusable connection path.

Your credentials remain outside the source code. The client resolves them from your local AWS session.

  • Save src/client.js.
  • Confirm that client.js appears directly beneath src in the file list.

Amazon SQS addresses each queue through a queue URL. A shared helper can retrieve that URL before another script sends or receives a message.

  • Use the file-creation control in the VS Code file list to create src/queue.js inside the existing src folder.
  • Paste the queue URL helper into src/queue.js:
import { GetQueueUrlCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { ORDER_QUEUE_NAME } from "./config.js";

export async function getOrderQueueUrl() {
  const { QueueUrl } = await sqsClient.send(
    new GetQueueUrlCommand({ QueueName: ORDER_QUEUE_NAME }),
  );

  if (!QueueUrl) {
    throw new Error("Amazon SQS did not return the order queue URL.");
  }

  return QueueUrl;
}

What does this helper do?

  • The GetQueueUrlCommand request looks up the queue named by ORDER_QUEUE_NAME.
  • The QueueUrl check stops the script when Amazon SQS does not provide an address.
  • The exported getOrderQueueUrl() function gives later scripts the validated URL.
  • Save src/queue.js.
  • Confirm that queue.js appears directly beneath src in the file list.

Do the shared modules look disconnected?

Check that config.js sits directly inside src. Check the same location for client.js and queue.js.

Compare the local imports with ./client.js and ./config.js. Help me verify my shared SQS modules.

Create the Standard queue

A Standard queue provides at-least-once delivery. This behavior makes message retries observable when you build the worker.

The setup script creates nextwork-orders in your verified AWS account. Omitting a FIFO setting creates the Standard queue used throughout this project.

  • Use the file-creation control in the VS Code file list to create src/setup-queue.js inside the existing src folder.
  • Paste the queue creation script into src/setup-queue.js:
import { CreateQueueCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { ORDER_QUEUE_NAME } from "./config.js";

async function main() {
  const { QueueUrl } = await sqsClient.send(
    new CreateQueueCommand({ QueueName: ORDER_QUEUE_NAME }),
  );

  console.log(`Queue ready: ${QueueUrl}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

What does this setup script do?

  • The CreateQueueCommand request creates the queue using ORDER_QUEUE_NAME.
  • The returned QueueUrl identifies the queue for future requests.
  • The error handler records a failing process exit when the request does not succeed.
  • Save src/setup-queue.js.

Before you create the queue, what confirmation do you expect from the script? Make your prediction before continuing.

  • Create the queue from the terminal in sqs-order-processor by running this command:
node src/setup-queue.js

What should you see?

Node.js runs src/setup-queue.js as the entry point. The terminal prints Queue ready: followed by an SQS queue URL.

That URL confirms that AWS created or found nextwork-orders in your configured Region.

  • Wait at least one second before using the new queue.

That is your first cloud resource online. The nextwork-orders queue is ready to accept a message.

Did the queue setup fail?

Confirm that your AWS session from the previous step remains valid. An expired session prevents the SDK from authenticating.

Confirm that your shared AWS configuration still contains a Region. The SQS client needs a Region to select an endpoint.

Still stuck? Help me diagnose why my setup script cannot create the SQS queue.

Send your first JSON order

A producer turns an in-memory order into a message body. The queue stores that message independently until a consumer receives it.

Why use a local producer?

A local producer keeps the send request visible in your own code. You can see the exact order object before Amazon SQS stores it.

This direct path prepares you to compare the producer with the separate worker. The separation makes producer-consumer decoupling concrete.

  • Use the file-creation control in the VS Code file list to create src/producer.js inside the existing src folder.
  • Paste the producer into src/producer.js:
import { SendMessageCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  const order = {
    orderId: `order-${Date.now()}`,
    item: "Mechanical keyboard",
    quantity: 1,
  };

  await sqsClient.send(
    new SendMessageCommand({
      QueueUrl: queueUrl,
      MessageBody: JSON.stringify(order),
    }),
  );

  console.log(`Queued ${order.orderId}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

What does this producer do?

  • The orderId uses Date.now() to give the order a recognizable identifier.
  • The item field names the requested product.
  • The quantity field records how many units the order requests.
  • The SendMessageCommand request sends the serialized object as the MessageBody.
  • The final log exposes the same generated ID for the worker demonstration.
  • Save src/producer.js.
  • Confirm that producer.js appears directly beneath src in the file list.

✔️ Awesome, I've got everything!

Your configuration module, client, queue helper, setup script, and producer now form one complete sending path.

ⓧ I'd like to double check the full code

Compare each file with the complete versions below. Keep every filename plus each capital letter exactly as shown.

{
  "name": "sqs-order-processor",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "@aws-sdk/client-sqs": "3.1146.0"
  }
}

Why package.json matters

This file enables ECMAScript modules. It also pins the SQS client dependency used by the scripts.

export const ORDER_QUEUE_NAME = "nextwork-orders";

Why config.js matters

This file keeps the source queue name consistent throughout the project.

import { SQSClient } from "@aws-sdk/client-sqs";

export const sqsClient = new SQSClient({});

Why client.js matters

This file exports the shared SQS client that uses your local AWS configuration.

import { GetQueueUrlCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { ORDER_QUEUE_NAME } from "./config.js";

export async function getOrderQueueUrl() {
  const { QueueUrl } = await sqsClient.send(
    new GetQueueUrlCommand({ QueueName: ORDER_QUEUE_NAME }),
  );

  if (!QueueUrl) {
    throw new Error("Amazon SQS did not return the order queue URL.");
  }

  return QueueUrl;
}

Why queue.js matters

This helper finds the source queue URL. It stops execution if Amazon SQS does not return one.

import { CreateQueueCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { ORDER_QUEUE_NAME } from "./config.js";

async function main() {
  const { QueueUrl } = await sqsClient.send(
    new CreateQueueCommand({ QueueName: ORDER_QUEUE_NAME }),
  );

  console.log(`Queue ready: ${QueueUrl}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Why setup-queue.js matters

This script creates the Standard source queue. Its output exposes the queue URL returned by Amazon SQS.

import { SendMessageCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  const order = {
    orderId: `order-${Date.now()}`,
    item: "Mechanical keyboard",
    quantity: 1,
  };

  await sqsClient.send(
    new SendMessageCommand({
      QueueUrl: queueUrl,
      MessageBody: JSON.stringify(order),
    }),
  );

  console.log(`Queued ${order.orderId}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Why producer.js matters

This script creates a uniquely identified order. It sends the serialized order to the source queue.

Before you run the producer, what output would prove that Amazon SQS accepted a uniquely identified order? Make your prediction before continuing.

  • Send one order from the terminal in sqs-order-processor by running this command:
node src/producer.js

What should you see?

Node.js runs src/producer.js as the entry point. The terminal prints Queued order- followed by the generated timestamp digits.

The completed send request means the serialized order is visible in nextwork-orders.

You now have a live producer handoff. The order is waiting independently inside Amazon SQS for a worker to receive it.

Did the producer fail to queue the order?

Confirm that the setup script printed a queue URL before you ran the producer. The URL helper needs the source queue to exist.

Compare each local import with the full-code tab. A missing .js extension prevents the module from loading.

Still stuck? Help me debug why my SQS producer cannot send its first order.

Your independent producer can now place real orders onto the queue. Next, you will receive this order without deleting it and watch Amazon SQS deliver it again.

Watch an Undeleted Order Return

Your producer has placed a real JSON order onto Amazon SQS. The queue now has work that a separate consumer can receive.

A receive call hides a message for a visibility timeout. This step uses an intentionally incomplete worker to test whether processing alone acknowledges the order.

In this step, get ready to:
  • Create a one-message worker with a 20-second visibility timeout.
  • Observe the queue while the received order is temporarily hidden.
  • Confirm the identical order ID returns after the timeout.
Build the deliberately incomplete worker

This worker requests one message from the queue. It parses the message body so you can identify the order that was received.

  • Switch back to the existing sqs-order-processor folder in Visual Studio Code.
  • Create naive-worker.js inside the existing src folder by using the Explorer sidebar.

You should see naive-worker.js listed beside the other JavaScript files in src.

  • Add the one-message receive logic by pasting this code into src/naive-worker.js:
import { ReceiveMessageCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  const { Messages = [] } = await sqsClient.send(
    new ReceiveMessageCommand({
      QueueUrl: queueUrl,
      MaxNumberOfMessages: 1,
      VisibilityTimeout: 20,
    }),
  );

  if (Messages.length === 0) {
    console.log("No order returned.");
    return;
  }

  const order = JSON.parse(Messages[0].Body);
  console.log(`Processed without deleting: ${order.orderId}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

What does this worker do?

  • The ReceiveMessageCommand request asks the queue for at most one message.
  • The VisibilityTimeout value temporarily hides the received message for 20 seconds.
  • The Messages default handles an empty receive without crashing the worker.
  • The body parser turns the stored JSON string back into an order object.
  • Save src/naive-worker.js.

Before you run this worker, do you think printing a processed order is enough to remove it from the queue?

  • Test the worker from the sqs-order-processor terminal by running this command:
node src/naive-worker.js

The worker may print No order returned. because the receive uses short polling. A successful receive starts with Processed without deleting:.

  • Repeat the same command until the worker prints a processed order ID.
  • Record the printed order ID here: your processed order ID.

You have your first consumer result. That order ID is now your reference for the retry test.

✔️ Awesome, I've got everything!

Great. Confirm src/naive-worker.js is saved. Your terminal should also show an order ID after Processed without deleting:.

ⓧ I'd like to double check the full code

Compare your saved src/naive-worker.js file with this complete version.

import { ReceiveMessageCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  const { Messages = [] } = await sqsClient.send(
    new ReceiveMessageCommand({
      QueueUrl: queueUrl,
      MaxNumberOfMessages: 1,
      VisibilityTimeout: 20,
    }),
  );

  if (Messages.length === 0) {
    console.log("No order returned.");
    return;
  }

  const order = JSON.parse(Messages[0].Body);
  console.log(`Processed without deleting: ${order.orderId}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Repeatedly seeing no order?

  • Confirm the producer terminal from the previous step printed Queued followed by an order ID.
  • Wait 20 seconds if this order was received during an earlier attempt. It remains hidden until that visibility timeout expires.

Still stuck? Help me work out why my naive SQS worker keeps returning no order.

Check the invisible period

The first successful receive made the order temporarily invisible. Short polling checks a sample of queue servers before returning immediately.

Before you run the worker again, do you expect the same order to be available immediately?

  • Check the queue during the visibility timeout by immediately running the worker again:
node src/naive-worker.js

You should see No order returned. while the received order remains inside its 20-second visibility timeout.

What did the empty receive prove?

The earlier receive temporarily hid the order from other receive requests. The worker exited after processing one result.

Short polling can also return an empty response when a visible message exists in a small queue. A single empty response never proves that the queue has no messages.

Prove the order returns

The 20-second wait can feel slow during a terminal test. That pause is the queue protecting the message from immediate duplicate processing.

  • Wait at least 20 seconds from the first successful receive.

Before you run the worker again, do you expect the original order ID or a new order ID?

  • Check for redelivery by running the worker again:
node src/naive-worker.js

Short polling may still produce No order returned. on an individual attempt.

  • Repeat the same command until the worker prints an order ID.

You'll see Processed without deleting: followed by your processed order ID. That matching ID proves the earlier receive never acknowledged the message.

That is the retry lifecycle made visible. The same order became available again after its visibility timeout expired.

Did a different order return?

  • Measure the 20 seconds from the first line that begins with Processed without deleting:.
  • Continue running the worker until the recorded order ID is printed again.
  • Compare the full generated ID because every produced order has its own timestamp.

Need help tracing the retry? Help me verify why my original SQS order has not returned after its visibility timeout.

You have proved that a processed order can return when the consumer never acknowledges it. Next, you'll replace this one-shot test with a long-polling worker that deletes each successful message.

Build a Long-Polling Worker That Acknowledges Success

You proved that an undeleted order returns after its visibility timeout. That retry behavior makes every acknowledgement decision matter in Amazon SQS.

Now you'll replace short polling with long polling in a continuous worker. The worker will delete each successfully processed message with its latest receipt handle.

In this step, get ready to:
  • Build a continuous worker that waits for orders with long polling.
  • Acknowledge each successful order with its latest receipt handle.
  • Add a cleanup script for the persistent queue.
Create the long-polling loop

A long-poll receive request waits for up to 20 seconds when the queue is empty. This reduces the immediate empty responses you saw with short polling.

  • In the Explorer sidebar in VS Code, create a file named worker.js inside the src folder.
  • Paste this first runnable polling loop into worker.js:
import {
  DeleteMessageCommand,
  ReceiveMessageCommand,
} from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  console.log("Worker is long polling for orders. Press Control-C to stop.");

  while (true) {
    const { Messages = [] } = await sqsClient.send(
      new ReceiveMessageCommand({
        QueueUrl: queueUrl,
        MaxNumberOfMessages: 1,
        WaitTimeSeconds: 20,
        VisibilityTimeout: 20,
      }),
    );
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

How does the polling loop work?

  • The imports load the receive command plus the delete command from the AWS SDK for JavaScript v3.
  • The while (true) loop starts another receive request whenever the previous request finishes.
  • The WaitTimeSeconds value keeps each request open for up to 20 seconds while it waits for an order.
  • The VisibilityTimeout value hides a received order for 20 seconds while the worker handles it.
  • Save worker.js.

Before you start the worker, do you expect the process to exit after one receive request or keep polling?

  • Start the worker from the existing project terminal by running:
node src/worker.js

What should you see?

You'll see Worker is long polling for orders. Press Control-C to stop. in the terminal. The process remains active because the receive loop keeps running.

  • Stop this first worker run by pressing Control-C.

Does the worker exit immediately?

  • Check that worker.js is inside the src folder.
  • Check each import path for matching quotation marks.
  • Confirm your terminal is still inside the sqs-order-processor folder.

Still stuck? Help me debug why my long-polling worker exits immediately.

Process and delete successful orders

The receive call returns an empty Messages array when no order arrives during a poll. The worker needs to report that result before beginning the next poll.

  • In worker.js, find the receive call that ends with );.
  • Add this empty-response branch directly below that receive call:
    if (Messages.length === 0) {
      console.log("No orders yet. Polling again.");
      continue;
    }

What does this branch do?

  • The length check detects a completed poll that returned no messages.
  • The continue statement begins the next pass through the receive loop.
  • Save worker.js.

An empty long poll can stay quiet for up to 20 seconds. That pause means the worker is waiting for an order.

Before you run this version, what message do you expect after an empty polling cycle?

  • Run the updated worker until one empty polling cycle completes:
node src/worker.js

What should you see now?

You'll see No orders yet. Polling again. after an empty poll. The worker immediately starts waiting again.

  • Stop the worker by pressing Control-C after the empty-poll message appears.

Waiting Longer Than 20 Seconds?

  • Check that WaitTimeSeconds: 20 appears inside ReceiveMessageCommand.
  • Check that the empty-response branch remains inside the while (true) loop.

Need another pair of eyes? Help me find why my worker does not report empty long polls.

A successful receive still needs an explicit acknowledgement. The worker will parse the order before deleting the message with the latest message.ReceiptHandle.

  • In worker.js, locate the closing brace of the empty-response branch.
  • Add this processing loop directly below that branch:
    for (const message of Messages) {
      const order = JSON.parse(message.Body);
      console.log(`Processed ${order.orderId}: ${order.quantity} x ${order.item}`);

      await sqsClient.send(
        new DeleteMessageCommand({
          QueueUrl: queueUrl,
          ReceiptHandle: message.ReceiptHandle,
        }),
      );

      console.log(`Acknowledged and deleted: ${order.orderId}`);
    }

How does acknowledgement work?

  • The worker parses each message body from JSON into an order object.
  • The first log confirms that the worker processed the order data.
  • The delete command sends the current message's latest receipt handle to the queue.
  • The final log confirms that the acknowledgement request completed.
  • Save worker.js.

✔️ Awesome, I've got everything!

Your worker.js file now contains the continuous receive loop plus successful-message deletion.

ⓧ I'd like to double check the full code

import {
  DeleteMessageCommand,
  ReceiveMessageCommand,
} from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  console.log("Worker is long polling for orders. Press Control-C to stop.");

  while (true) {
    const { Messages = [] } = await sqsClient.send(
      new ReceiveMessageCommand({
        QueueUrl: queueUrl,
        MaxNumberOfMessages: 1,
        WaitTimeSeconds: 20,
        VisibilityTimeout: 20,
      }),
    );

    if (Messages.length === 0) {
      console.log("No orders yet. Polling again.");
      continue;
    }

    for (const message of Messages) {
      const order = JSON.parse(message.Body);
      console.log(`Processed ${order.orderId}: ${order.quantity} x ${order.item}`);

      await sqsClient.send(
        new DeleteMessageCommand({
          QueueUrl: queueUrl,
          ReceiptHandle: message.ReceiptHandle,
        }),
      );

      console.log(`Acknowledged and deleted: ${order.orderId}`);
    }
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The order from your earlier redelivery test may still be waiting in the queue. Its 20-second visibility timeout can create a short pause before this worker receives it.

Before you run the completed worker, what should happen to that order after the processing log appears?

  • Run the completed worker until it acknowledges the earlier order:
node src/worker.js

What proves deletion worked?

You'll see a Processed line for the order. You'll then see Acknowledged and deleted: followed by the same order ID.

  • Stop the worker by pressing Control-C after the acknowledgement appears.

Order Processed but Not Acknowledged?

  • Check that ReceiptHandle uses message.ReceiptHandle from the current loop iteration.
  • Check that DeleteMessageCommand receives the same queueUrl used by the receive command.
  • Check that the delete request includes await before the acknowledgement log.

Still seeing the order return? Help me debug why my SQS worker processes a message without deleting it.

Prepare cleanup before the final test

The cleanup script contains a real queue deletion. You are only saving it now, so the queue stays available for the final test.

  • In the Explorer sidebar, create cleanup.js inside the src folder.
  • Paste this cleanup script into cleanup.js:
import { DeleteQueueCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  await sqsClient.send(new DeleteQueueCommand({ QueueUrl: queueUrl }));
  console.log("Deleted nextwork-orders.");
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

What does the cleanup script do?

  • The script retrieves the URL for nextwork-orders.
  • The delete command removes that queue from the verified AWS account and Region.
  • The confirmation log reports the queue name after the deletion request completes.
  • Save cleanup.js.
  • Confirm that cleanup.js appears under src in the Explorer sidebar.

Don't See the Cleanup File?

  • Check that the file is named exactly cleanup.js.
  • Check that the file appears inside src beside worker.js.

Need help locating it? Help me confirm that cleanup.js is saved in the correct project folder.

✔️ Awesome, I've got everything!

Your cleanup script is saved. Keep it available for the cleanup section.

ⓧ I'd like to double check the full code

import { DeleteQueueCommand } from "@aws-sdk/client-sqs";
import { sqsClient } from "./client.js";
import { getOrderQueueUrl } from "./queue.js";

async function main() {
  const queueUrl = await getOrderQueueUrl();
  await sqsClient.send(new DeleteQueueCommand({ QueueUrl: queueUrl }));
  console.log("Deleted nextwork-orders.");
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The final check uses two terminals so the producer can submit orders while the worker keeps polling. Each terminal shows one side of the producer-consumer flow.

  • Return to the first terminal from your worker tests.

Before you start the worker, how many acknowledgement lines should appear after the producer submits two orders?

  • Start the worker in the first terminal by running:
node src/worker.js

The worker is ready

You'll see Worker is long polling for orders. Press Control-C to stop. while the process waits for producer messages.

  • Create a second terminal session from the VS Code terminal panel.
  • Queue two fresh orders from the second terminal by running this command twice:
node src/producer.js

What should the producer show?

Each run prints Queued order- followed by a generated order ID. You should have two different order IDs after the second run.

  • Switch back to the first terminal.
  • Confirm that each generated order ID appears in a Processed line.
  • Confirm that each generated order ID appears again after Acknowledged and deleted:.
  • Stop the worker by pressing Control-C after both acknowledgements appear.

That's the full message lifecycle working. Your producer submits independent orders while the worker long polls, processes each order, and acknowledges success through deletion.

Your order processor now handles successful work without leaving messages behind. The optional Secret Mission takes this retry-aware design one step further.

Secret mission

Quarantine a Poison Order in a Dead-Letter Queue

A permanently failing order can cycle through a source queue forever. Configure a dead-letter queue to isolate that poison order after repeated receives while successful orders continue through the normal path.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the two queues available, stop your local Node.js worker for now, or remove both queues. Amazon Simple Queue Service (Amazon SQS) includes 1 million free requests each month.

Cost warning

Every Amazon SQS API action counts as a request. Request charges can apply after your account consumes its monthly free allowance.

  • Choose the Pause or Delete option if you do not plan to continue the project today.

Resources you used:

  • Standard source queue nextwork-orders with a RedrivePolicy targeting the dead-letter queue.
  • Standard dead-letter queue nextwork-orders-dlq containing the quarantined poison order.

Keep everything running

No queue deletion is needed. Choose this option if you plan to repeat the order-processing demonstration.

  • Stop the worker by pressing Control-C in its terminal.
  • Leave nextwork-orders available in your verified account.
  • Leave nextwork-orders-dlq available in your verified account.
  • Keep the local sqs-order-processor folder in place.

Your source queue keeps its dead-letter redrive policy. Future requests from your scripts continue to count toward monthly usage.

Pause - I'll come back to this later

Pausing this project stops every local process while preserving both queues. Your queue configuration remains ready for your return.

  • Return to each terminal associated with the sqs-order-processor project.
  • Press Control-C in any terminal that still has a running process.
  • Close each project terminal after its process has stopped.
  • Leave both queues unchanged in your verified account.

The stopped worker makes no further polling requests. Both queues can still retain messages while you are away.

Delete - I don't want to use this again

Deleting both queues is permanent. Your local project files remain available for reference.

  • Stop each running worker by pressing Control-C in its terminal.
  • Switch back to the terminal inside your sqs-order-processor folder.
  • Delete both queues by running this command:
node src/cleanup.js

What does this cleanup do?

The script finds the current URLs for nextwork-orders and nextwork-orders-dlq. It sends DeleteQueueCommand for each URL.

  • Confirm that the terminal prints Deleted nextwork-orders and nextwork-orders-dlq.
  • Wait at least 60 seconds before recreating either queue with the same name.

You have closed the loop on the cloud side. Both persistent queues are deleted while your local project remains available for reference.

Cleanup command failed?

  • Renew your session if it no longer points to the verified account.
  • Restore the verified Region if your shared configuration changed.
  • Use the help link below if one queue was already deleted before the script stopped.

Help me diagnose the cleanup failure.

Nice Work!

Nice Work!

You did it! You built a retry-aware JavaScript order processor with Amazon SQS.

Your terminal demo now follows an order from submission through processing. It also exposes retries before isolating a permanently failing order.

You've learned how to:

  • Built an independent producer and consumer that demonstrate producer-consumer decoupling through a Standard queue.
  • Proved at-least-once delivery by watching an undeleted order return after its visibility timeout.
  • Built a long-polling worker that acknowledges each successful order through receipt-handle deletion.
  • Completed the Secret Mission by routing a poison order into a dead-letter queue after repeated failed receives.

Ready to quiz yourself?