Library / SDK
bbc/sqs-consumer avatar
bbc/sqs-consumer

sqs-consumer: what its return values do to your SQS messages, and where the FIFO caveat bites

Build Amazon Simple Queue Service (SQS) based applications without the boilerplate

1,908 stars336 forksTypeScriptNOASSERTION

At a glance

What is it?
sqs-consumer is the BBC's TypeScript library that turns a long-polling loop and a delete call into a single async handler function. The convenience that removes boilerplate also moves message acknowledgement into your return value, which is the detail most adopters get wrong, and FIFO queue support is explicitly untested.
Who is it for?
sqs-consumer suits Node services where a handler function is a natural fit for processing SQS messages and where at-least-once redelivery through a dead letter queue is acceptable. It does not suit a strict FIFO ordering requirement, because the maintainers state they do not explicitly test FIFO queues and the library only warns you.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • bbc/sqs-consumer1,908 ★ · +0
Star history
Repository2026-10-162026-10-23
bbc/sqs-consumer1.9k1.9k

0 Stars in the last 7 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The whole library is one async function and a return value

The core idea is that you write a handler and the library handles polling, visibility and deletion. The README's opening example creates a consumer with a queue URL and a handler, subscribes to error events, and starts polling:

js
import { Consumer } from "sqs-consumer";

const app = Consumer.create({
  queueUrl: "https://sqs.eu-west-1.amazonaws.com/account-id/queue-name",
  handleMessage: async (message) => {
    // do some work with `message`
  },
});

app.on("error", (err) => {
  console.error(err.message);
});

app.start();

`Consumer.create(options)` builds the consumer, `consumer.start()` begins polling the queue, and `consumer.stop(options)` stops it. That surface is deliberately small. The full option list lives in the generated API documentation at bbc.github.io/sqs-consumer rather than in the README.

The package is ESM-first in its own metadata: type is set to module, and exports are split into an import path pointing at dist/esm and a require path pointing at dist/cjs, each with its own type declarations. That dual build exists for consumers who have not finished moving off CommonJS, and it means type resolution differs between the two entry points.

Returning nothing means retrying forever

This is the part worth internalizing before anything else. With the default `alwaysAcknowledge: false`, the return value of your handler decides the fate of the message.

Returning `undefined`, an empty object `{}` or an empty array `[]` leaves the message on the queue. The message is not deleted, so SQS will redeliver it. For batch handling with `handleMessageBatch`, returning `undefined` or `[]` prevents acknowledgment of all messages in the batch. For single message handling, `undefined` or `{}` prevents acknowledgment.

Returning the message object, or the array of messages in the batch case, acknowledges and deletes it. The README is explicit that only the message ids you actually return are deleted, which is the mechanism behind partial batch acknowledgment: return the subset that succeeded and the rest stay queued.

Two footnotes matter for planning an upgrade. Returning `void` is discouraged and will be deprecated in a future release, and with `strictReturn: true` returning `null` throws. The README says `strictReturn` will become the default behavior in a future release, so a handler that currently returns `null` on an error path will break rather than silently retry when that default lands.

Parallelism, ordering, and what a thrown error costs you

By default messages are processed one at a time: a new message is not received until the first has been processed. To process in parallel you raise the `batchSize` option. The README adds a warning that is easy to skim past: you must await any processing you start inside the handler, or the one-at-a-time guarantee you thought you had is gone.

Errors are handled by leaving the message in place. Throwing, or returning a rejected promise, causes the message to be left on the queue. What eventually removes it is your infrastructure: the README points at an SQS redrive policy for moving messages that cannot be processed to a dead letter queue. So retry exhaustion is not something sqs-consumer decides for you, it is something you configure on the queue.

Message visibility is the other half of the retry story, and it is in the IAM list rather than the README prose. Alongside receive and delete, the consumer calls `sqs:ChangeMessageVisibility` and `sqs:ChangeMessageVisibilityBatch`, which means it can extend the invisibility timeout on a message it is still working through. A handler that runs longer than the queue's visibility timeout will see the message redelivered while it is still processing, so a slow handler plus a short visibility timeout means duplicate work by design.

FIFO queues are supported on the README's own condition

Using sqs-consumer with a FIFO queue produces a warning in your logs. The README explains why: the maintainers do not explicitly test sqs-consumer with FIFO queues, so they cannot guarantee it will work as expected, though with the correct configuration it should.

If you have configured it, observed that FIFO ordering behaves as expected, and want the warning gone, set `suppressFifoWarning: true`.

The related rule is the one to remember: to maintain FIFO ordering you should always use `handleMessageBatch` instead of `handleMessage`. That follows from the single-message default of one at a time, which only gives you ordering within one message. This is the library's clearest statement of where its guarantees stop, and it is worth treating as a boundary rather than a caveat.

Credentials and the SQSClient you hand it

By default the consumer looks for AWS credentials wherever the AWS SDK looks. The simplest route is environment variables:

bash
export AWS_SECRET_ACCESS_KEY=...
export AWS_ACCESS_KEY_ID=...

When you need explicit credentials or a specific region, pass a pre-configured client from @aws-sdk/client-sqs:

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

const app = Consumer.create({
  queueUrl: "https://sqs.eu-west-1.amazonaws.com/account-id/queue-name",
  handleMessage: async (message) => {
    // ...
  },
  sqs: new SQSClient({
    region: "my-region",
    credentials: {
      accessKeyId: "yourAccessKey",
      secretAccessKey: "yourSecret",
    },
  }),
});

That example subscribes to a third event, `timeout_error`, alongside `error` and `processing_error`. Event naming is the main observability surface the README shows, and in a serverless function the timeout event is often the one you most want logged.

Stopping is where deployments get surprising

Calling `consumer.stop()` alone only prevents new polls from being scheduled. The consumer emits `stopped` immediately, even if the current long poll or a message handler is still running. In-flight work is not awaited unless you ask for it.

By default `abort` is `false`, which means requests already made to SQS will still complete. If you want those aborted instead, pass the abort value as true:

js
consumer.stop({ abort: true })

For a graceful shutdown that waits, set `pollingCompleteWaitTimeMs` to the longest wait you are willing to accept. While waiting, the consumer emits `waiting_for_polling_to_complete` every second, and if the timeout elapses first, `waiting_for_polling_to_complete_timeout_exceeded` fires immediately before `stopped`. The README's guidance for graceful shutdowns is to keep `abort: false`.

Put together with the visibility timeout point above, this is where a naive deployment drops messages: `stopped` firing early gives your runtime the impression that processing finished while a handler is still running.

Where this library is the wrong choice

The honest limitation list is short but real.

FIFO is the first. Ordering-critical pipelines should not be built on a consumer whose own docs decline to test the feature. Second, sqs-consumer is Node and TypeScript only. The repository also publishes to JSR at @bbc/sqs-consumer, but the runtime story is JavaScript, so a Python or Go shop is out.

Third, acknowledgement is implicit in a return value, which is idiomatic JavaScript and a genuine footgun for a handler that ends in an early `return` on a success branch. A handler that returns `undefined` after doing the work correctly will retry forever. The README anticipates this and is moving the library toward stricter returns, but the current default still puts the burden on the caller.

Fourth, the library does not decide retry limits. Without a redrive policy on the queue, a poison message cycles indefinitely. And there is no documented rollback or downgrade path in the README, so a major version bump is a migration you plan for, not something you discover.

Against the raw AWS SDK

The alternative is calling the SDK yourself. ReceiveMessage with WaitTimeSeconds for long polling, invoke your handler, then call DeleteMessage or ChangeMessageVisibility on the ids whose work succeeded. That is roughly what this library does, and the SDK gives you the same five API calls the README asks you to permit.

The difference is the loop. sqs-consumer owns scheduling, in-flight accounting, the graceful shutdown sequence and the event stream. Writing that correctly is not hard once and easy to get subtly wrong, particularly the part where a stopped consumer must not leave a long poll dangling. If you need none of that, a direct SDK handler inside your existing runtime is a defensible choice and one fewer dependency.

The stronger argument for the library is operational rather than functional. Backed by the BBC and released through v15.0.4 on 2026-09-25 after v15.0.3 in July and v15.0.2 in May, with the last push on 2026-10-12, it is a maintained component rather than a snippet you copied. The catch is its license, which the repository declares Apache-2.0 in package.json while GitHub reports the LICENSE file as a custom license it could not classify. Read the LICENSE file before shipping it inside a product, because the SPDX identifier and the file are not obviously saying the same thing.

Editorial conclusion

sqs-consumer suits Node services where a handler function is a natural fit for processing SQS messages and where at-least-once redelivery through a dead letter queue is acceptable. It does not suit a strict FIFO ordering requirement, because the maintainers state they do not explicitly test FIFO queues and the library only warns you. Before adopting, decide whether your handler will return the message it processed or nothing at all, since that single choice determines whether messages are deleted or retried forever, and grant exactly the five IAM actions the consumer calls on your queue.

Frequently asked questions

How do I install sqs-consumer?

Run `npm install sqs-consumer` in your project, or use the variant of whatever package manager you prefer. The package is also published to JSR as @bbc/sqs-consumer if you prefer that registry.

How do I delete a message after processing it in sqs-consumer?

Return the message object from `handleMessage`, or the array of handled messages from `handleMessageBatch`. With the default `alwaysAcknowledge: false`, returning `undefined`, `{}` or `[]` leaves the message on the queue for retry instead of deleting it.

Does sqs-consumer work with FIFO queues?

The README states the maintainers do not explicitly test sqs-consumer with FIFO queues and cannot guarantee expected behavior, so you will see a warning in the logs. To keep FIFO ordering you should use `handleMessageBatch` rather than `handleMessage`, and `suppressFifoWarning: true` silences the log once you have verified it yourself.

Which AWS IAM permissions does sqs-consumer need?

The README lists `sqs:ReceiveMessage`, `sqs:DeleteMessage`, `sqs:DeleteMessageBatch`, `sqs:ChangeMessageVisibility` and `sqs:ChangeMessageVisibilityBatch`, all of which must be granted on the queue being consumed.

How do I shut down sqs-consumer gracefully?

Keep `abort: false` and set `pollingCompleteWaitTimeMs` to the longest wait you will accept. Without it, `consumer.stop()` only stops scheduling new polls and emits `stopped` while a long poll or handler may still be running.

Official sources

  1. bbc/sqs-consumer on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/bbc-sqs-consumer.svg)](https://hysenlabs.com/projects/bbc-sqs-consumer)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/bbc-sqs-consumer.svg)](https://hysenlabs.com/projects/bbc-sqs-consumer)

Was this useful?