Event Pipeline Manual 3D app 2D app Contents
Manual · ASD-STE100 Simplified Technical English

Event pipeline simulation

SNS → SQS → Avro → ActiveMQ Artemis → API → Aurora PostgreSQL, in two AWS regions

Applicability

SystemThe web app event-pipeline-v2. It is a Cloudflare Worker. It serves static assets, and a small API for the notes in this manual.
Pages/ (3D view), /2d (2D view), /manual (this manual).
ModelAn event pipeline in AWS us-east-1 and us-east-2. Its parts are SNS, SQS, KEDA pods, Avro, PrivateLink, a Network Load Balancer and an ActiveMQ Artemis cluster. Each cluster has a mirror to the other region. After the Artemis consumers, each region has an API (1 to 10 pods, HorizontalPodAutoscaler). The API stores each event in an Aurora PostgreSQL Global Database (Serverless v2). The primary cluster in us-east-1 has the writer e1-1 and the reader e1-2. The secondary cluster in us-east-2 has the read-only reader e2-1. Each region also has a DLQ processor with an S3 bucket, an ALB in front of the API, and a read path: Center, Cube and a Query API (1 to 5 pods, HorizontalPodAutoscaler).
App versionProject event-pipeline-v2-3d, package version 0.0.0. The project has no release numbers.
Data dateResearch notes: 2026-10-01, with later additions up to 2026-11-03. Scenario results: 2026-11-02 for scenarios 1 to 31, 2026-11-03 for scenarios 32 to 43. Manual: 2026-11-03.
ReadersEngineers who build or operate this pipeline. Users of the app.

How to use this manual

  1. Read Section 1 to get an overview of the system.
  2. Use Section 2 to find data about one part. Each part has the same id as in the app, for example e1-sqs.
  3. Use Section 3 to learn how the system operates. Each subsection agrees with one step of the guided tour.
  4. Use Section 4 to do a task in the app or in the project.
  5. Use Section 5 when the app or the pipeline shows a problem.
  6. Use Section 6 for numbers, Section 7 for terms and Section 8 for sources.
  7. To change the theme, the font, the text size or the line width, press ⌘P (Ctrl+P on Windows and Linux), or click Aa at the top of the page.
  8. To add a highlight or a note, select text. To see all your notes, click ✎ Notes. If you are not signed in, notes stay in this browser. If you sign in, they sync to your account, and you see them on each device. To sign in, click Sign in to sync in the notes panel.

Values with the label Simulation are illustrative values of the app. They are not AWS, KEDA, Kubernetes or Artemis defaults.

Warnings, cautions and notes

WARNING

A warning tells you about a condition that can cause loss of messages, loss of stored rows or duplicate processing in a real pipeline.

CAUTION

A caution tells you about a condition that can cause incorrect results in the app, a failed check or a change to a live deployment.

NOTE

A note gives you more data. The data helps you to do a task or to understand the system.

A warning or a caution comes before the step that it applies to.

1Description of the system

1.1Purpose

The app shows how one event moves through an event pipeline in two AWS regions. At the end of the pipeline, an API stores the event in an Aurora PostgreSQL database. The app also shows what occurs when a part of the pipeline or the database fails. The default view also shows a read path: two UIs that query the stored data. The Show control hides the read path.

A live simulation runs in the app. You can change the traffic, the topology, the failures, the API and the database in the Simulate panel. Two pages show the same simulation: a 3D page and a 2D page.

1.2Message path in short

  1. The producer publishes an event to an SNS topic.
  2. SNS sends a copy of the event to each subscribed SQS queue.
  3. A pod asks SQS for a message with an HTTP request, and the answer carries the message. This is a pull. KEDA sets the number of pods.
  4. The pod puts the SQS body into an Avro record.
  5. The pod sends the Avro message over a Core connection, through PrivateLink and a Network Load Balancer (NLB), to one Artemis broker.
  6. The pod deletes the SQS message only after the broker confirms the send.
  7. The Artemis cluster puts the message on a broker that has consumers.
  8. The broker sends a copy of the message to its paired broker in the other region. This copy is the mirror copy.
  9. A broker sends the message over a Core connection to one of 10 consumers. This is a push. The consumer does its own work. Then it sends an HTTP request to an API pod in its region.
  10. The API pod writes the event to the one Aurora writer: INSERT … ON CONFLICT (event_id) DO NOTHING. A request from us-east-2 crosses the region for this write.
  11. The writer commits the row. Aurora replicates the row to the readers.
  12. The API sends a 2xx response. The consumer then sends an acknowledgement (ack). The broker also mirrors the ack.
  13. If the API sends a 4xx or a 5xx response, the consumer rolls back. The broker delivers the message again, or moves it to the dead-letter address.
NOTE

For an event, the API by default only writes. With Read before write on, the API first reads its local reader. If the row is already there, the API sends 200 and does not write. Refer to 3.18.

The read path is separate from this message path. It only reads:

  1. Center or Cube sends a query to the Query API of its region. Center and Cube are two separate deployments.
  2. A Query API pod sends the query through the ALB to an API pod. The consumers use the same ALB and the same API pods.
  3. The API pod reads the reader of its region.
  4. The answer goes back on the same path.

1.3Block diagram

Block diagram of the event pipeline in two regions The producer publishes to the SNS topic in each region. Each topic sends to an SQS queue and to the queue of another team. The SQS queue has a DLQ. KEDA pods convert messages to Avro and send them through PrivateLink and the NLB to an Artemis cluster of three brokers. Ten consumers read from the cluster. The cluster has a dead-letter address. Each broker mirrors to the broker with the same number in the other region. In one-topic mode, a cross-region subscription connects the us-east-1 topic to the us-east-2 queue. The consumers send HTTP requests to the API of their region and get a response. The us-east-1 API writes to the writer e1-1 in the Aurora primary cluster. The us-east-2 API writes across the region to the same writer. The writer replicates to the reader e1-2 in its cluster and to the read-only reader e2-1 in the Aurora secondary cluster in us-east-2. Dashed lines from each API to its local reader are the optional read before write. us-east-1 (N. Virginia) us-east-2 (Ohio) ProducerSNS Publish SNS topicfan-out Other teamqueue (copy) SQS queuestandard SQS DLQredrive PodsKEDA · Avro PrivateLinkNLB Artemis cluster Broker 1 Broker 2 Broker 3 10 consumersPL · NLB side Artemis DLQdead-letter addr. pull · HTTP Core push · Core SNS topicfan-out Other teamqueue (copy) SQS queuestandard SQS DLQredrive pull · HTTP Core push · Core PodsKEDA · Avro PrivateLinkNLB Artemis cluster Broker 1 Broker 2 Broker 3 10 consumersPL · NLB side Artemis DLQdead-letter addr. mirror B1⇄B1, B2⇄B2, B3⇄B3async, in two directions cross-region subscription (one-topic mode only) APIHPA 1–10 e1-2reader e1-1writer Aurora primaryServerless v2 APIHPA 1–10 Aurorasecondaryread-only e2-1reader Global Databasereplication, async cross-region write
Figure 1. Block diagram of the system. Labels above the lines give the protocol: pull over HTTP from SQS, Core to the brokers, and push over Core to the consumers. Refer to 1.7. Amber lines carry events. Violet lines carry Avro messages. Cyan lines are the mirror and the Aurora replication. Red dashed lines go to a dead-letter queue. Teal lines are HTTP requests to the API, and green lines are its 2xx responses. Light blue lines are SQL writes. A thin dashed light blue line is the optional read before write.

Figure 1 does not show the read path. Section 3.22 shows it. Figure 1 also does not show the DLQ processor, the S3 bucket or the ALB between the consumers and the API. Section 3.21 shows them. In the app, they are on the outer side of each region.

1.4Regions, queues and database

Each region has its own address and queue. The mirror copies messages under the names of their source region. Thus each region also holds the queue of the other region as a copy.

us-east-1 brokersus-east-2 brokers
Own traffic (consumed)use1.address → use1.queueuse2.address → use2.queue
Standby copy (not consumed)use2.queueuse1.queue

The consumers in each region read only the queue of their region. The mirrored ack deletes a standby copy. A consumer reads a standby copy only after a failover. In the two “Same queue name” modes of the app, both regions use the queue name events.queue.

Each region also has an API and an Aurora cluster. There is only one writer for the two regions. The table shows the start state of the app.

us-east-1us-east-2
APIe1-api: 1 to 10 pods (HPA)e2-api: 1 to 10 pods (HPA)
Aurora clusterPrimary cluster e1-aurora: writer e1-1, reader e1-2Secondary cluster e2-aurora: reader e2-1 (read-only)
Writes go toThe writer e1-1, in the same regionThe writer e1-1, across the region (cross-write)
Reads go to (queries from Center and Cube; consumer calls only with Read before write on)e1-2e2-1
Read pathe1-ui-center, e1-ui-cube, e1-query-apie2-ui-center, e2-ui-cube, e2-query-api
Rows get to the readers byShared cluster storage (sim: 0.15 s)Global Database replication aurora-repl (sim: Replication lag to us-east-2, default 0.8 s)

After a failover, the writer can be e1-2 or e2-1. The two APIs then write to that instance. The counter writer in the Simulate panel shows the current writer.

1.5Colour code

The app and the diagrams in this manual use the same colours. In print, the diagrams use darker shades of the same colours.

ColourMeaning
AmberEvent (SNS or SQS message)
Amber ringA pod asks SQS for a message (pull, HTTP). Refer to 1.7.
VioletAvro message in Artemis (push, Core)
CyanMirror copy, mirrored ack or Aurora replication
TealHTTP request from a consumer through the ALB to the API
Light blueSQL read or write from an API pod to an Aurora instance; stored rows
WhiteRead query on its way out: UI, Query API, ALB, API. The answer comes back green.
Red-orangePoison message (fails on each attempt)
RedDead-lettered, a 4xx or 5xx response, or failed
OrangeSQS queue and S3 bucket, objects in S3, a message on its way to S3, and the Center and Cube boxes
PeriwinkleQuery API pods
GreenProcessed: a 2xx response from the API. Also the answer to a query.
Light blue / pink / yellowConnection pinned to broker 1 / broker 2 / broker 3
Yellow (counter)The “duplicates” counter in the Simulate panel

Other display rules in the app:

  • In the SQS queue, a bright cube is a visible message. A dim cube is a message in flight.
  • A pod or a consumer shows red for a short time after a failure.
  • A broker that is down shows grey. Its stored messages also show grey.
  • Cubes on top of a broker are its stored messages. Violet cubes are messages of this region. Cyan cubes are mirror copies.
  • A consumer that waits for the API shows teal (2D view).
  • An API pod that starts shows hatched. A pod with requests in flight shows teal. Small marks on the pod show its in-flight requests (2D view).
  • An Aurora instance shows its role (WRITER, READER or PROMOTING…), an ACU gauge, its busy slots and its queued operations. Light blue cubes are its rows. A hollow cube is a row that has not replicated to this instance yet (2D view). The writer also shows its writes per second.
  • Center and Cube show the number of rows in their last answer. A short red outline shows a failed query.
  • A Query API pod uses the same display rules as an API pod, in periwinkle.

1.6Simulation values and assumptions

The rules in the app come from AWS, KEDA, Kubernetes, PostgreSQL and Artemis documentation. The times, rates and counts are illustrative, and simulation time is compressed. The app changes these values so that you can see the effects quickly:

ItemDocumented valueValue in the app
SQS visibility timeout30 s default3 s Simulation
SQS maxReceiveCountSet by you (no default in the sources)3 Simulation
Artemis max-delivery-attempts10 default3 Simulation
Artemis redelivery-delay0 default0 to 5 s, default 0 (panel slider) Simulation
KEDA polling interval30 s default2 s Simulation
Pod replicasKEDA default maximum 1001 to 10 (maximum 10 to 50 in the panel) Simulation
HPA sync period15 s default3 s (HPA_POLL) Simulation
HPA scale-down stabilization window300 s default10 s (HPA_DOWN) Simulation
HPA metric and targetSet by youIn-flight + queued requests for each pod, target 1 (HPA_TARGET) Simulation
API replicasSet by you (minReplicas, maxReplicas)1 to 10 for each region (maximum 10 to 50 in the panel) Simulation
Query API replicasSet by you (minReplicas, maxReplicas)1 to 5 for each region (maximum 5 to 50 in the panel) Simulation
Time until a new API pod is readyDepends on the image and the probes2 s (POD_START) Simulation
Requests at the same time in one API podDepends on the API2 (API_CONC) Simulation
Serverless v2 capacity range0.5 to 256 ACU, or 0 to 256 ACU with auto-pause (engine version)0.5 ACU to the Serverless v2 max capacity slider (1 to 32 ACU, default 8) Simulation
Reader lag in the same clusterUsually much less than 100 ms0.15 s (LOCAL_LAG) Simulation
Global Database replication lagTypically less than 1 s0.1 to 5 s, default 0.8 s (panel slider) Simulation
In-region writer failoverTypically less than 60 s, often less than 30 s3 s (DB_LOCAL_FAILOVER) Simulation
Global failover (promote the secondary)Minutes (RTO)5 s (DB_PROMOTE) Simulation

The model also uses these assumptions. The research notes give them because the original request does not specify them:

  • Each region has one SNS topic, and the producer publishes in its region. The panel has a switch for one topic to both regions.
  • Each region has 10 Artemis consumers.
  • The mirror pairs are broker N in us-east-1 and broker N in us-east-2.
  • The consumers get to the brokers through the same PrivateLink endpoint service as the pods.
  • Each region uses its own address and queue names (use1.* and use2.*). A failover makes the standby copy available to the other region.
  • The API is after the Artemis consumers. The consumers call it over HTTP. Its HPA metric is in-flight + queued requests for each pod, with target 1.
  • One Aurora writer serves the two regions. The us-east-2 API writes directly across the region. Write forwarding is the alternative.
  • For an event, the API by default only writes. The read before write is an option (Read before write, off by default).
  • Each region has one Center, one Cube and one Query API Deployment. The Query API calls the existing API through the existing ALB.
  • By default, each region sends 1 query each second, and Center and Cube take turns. Each query goes to the next ready Query API pod. The app draws no load balancer in front of the Query API.
  • A Cube query takes 4 times the time of a Center query on the reader, and costs 4 times the ACUs. A UI holds a maximum of 20 open queries. Above this limit, it skips the refresh.
  • After a failover, the old us-east-1 instances come back as readers that already have all rows. A real failover builds the old primary volume again.

Serverless v2 scaling in the app is a simple demand rule. Each write costs 0.45 ACU-seconds. Each read and each Center query costs 0.15 ACU-seconds. Each Cube query costs 0.6 ACU-seconds. Each second, the app divides the demand by the current ACUs. Above 70%, the instance grows by 50% (at least 0.5 ACU). Below 30% for 3 polls, it shrinks by 0.5 ACU for each poll. Above 100%, each statement takes longer, up to 4 times. Real Serverless v2 scales on CPU, memory and network load. Refer to 3.19.

1.7Pull and push

The two queues give messages to their clients in different ways. SQS is pull: the client asks. Artemis is push: the broker sends.

ItemSQS, to the podsArtemis, to the consumers
Who starts a transferThe pod. SQS never sends to a pod. [26]The broker, after the consumer opens its connection once. [26]
ProtocolHTTP. Each ask is an HTTPS POST to the SQS endpoint. [26]Core, the protocol of Artemis, over one long-lived TCP connection. [27]
One ask or transferReceiveMessage gets 1 to 10 messages. The default is 1. [26]The broker sends to a client buffer until it holds consumerWindowSize. The default is 1 MiB. [27]
Empty queueThe answer has no message, and the pod asks again. WaitTimeSeconds above 0 holds the answer open (a long poll). [26]The connection stays open. The broker sends when a message comes.
Slow clientThe client asks only when it is ready.The client buffer holds messages that other consumers could take. Set consumerWindowSize to 0 to stop this. [27]
In the appA PULL · HTTP label and an arrow from the pods to SQS. A hollow ring is an ask.A PUSH · Core label and an arrow from the brokers to the consumers. There are no rings.
Pull from SQS compared with push from Artemis At the top, a pod sends an HTTP request to the SQS queue, and the answer carries the message. This repeats for each message. At the bottom, a consumer opens one Core connection to a broker once. The broker then sends each message to the consumer with no request. Pull: SQS, over HTTP SQS queue Pod 1 · the pod asks: ReceiveMessage (each time) 2 · the answer carries the message Push: Artemis, over Core Broker Consumer 1 · the consumer connects (one time) 2 · the broker sends each message, with no ask
Figure 1-A. Pull and push. The dashed line shows who starts. A pod asks for each message. A consumer connects one time, and then the broker sends.

Simulation The rings are only a display. The app gives a pod its message with no time for the ask, and it gives a consumer one message at a time, as with a small consumerWindowSize. Thus the pull and the push move at the same speed in the app. The app does not show the cost of empty polls or of a full client buffer.

2Components

This section has one subsection for each part that you can click in the app. A part that exists in each region has one subsection for the two regions. The part id is the id that the app uses, for example in the URL parameter pin.

The prefix e1- identifies a part in us-east-1. The prefix e2- identifies a part in us-east-2. Use the “Show in app” links to open the part in the app with its info card.

Parts of the “Both regions (live)” view

2.1Producer

Part id
producer
Type
Publisher

The producer calls the SNS Publish action. In the default model, each region has its own topic, and the producer publishes to the topic in its region. In one-topic mode, the producer publishes to the us-east-1 topic. Each subscribed queue in the two regions then gets its own copy.

NOTE

In the simulation, the producer sends odd-numbered events to us-east-1 and even-numbered events to us-east-2. When a region is down, the producer sends to the topic in the other region. Simulation

2.2AWS region

Part ids
e1-plate (us-east-1, N. Virginia), e2-plate (us-east-2, Ohio)
Type
AWS region

The plate holds all parts that run in one region. Each region has its own SQS queues, DLQs, consumer pods, PrivateLink endpoint, Artemis cluster, API and Aurora cluster.

The Aurora cluster in us-east-1 is the primary cluster. It holds the one writer. The Aurora cluster in us-east-2 is the read-only secondary cluster. Its API sends writes to the writer in us-east-1.

2.3SNS topic

Part ids
e1-sns, e2-sns
Type
Amazon SNS standard topic

SNS sends a copy of each published message to each subscription. This is fan-out. SNS can also send to SQS queues in other regions.

Raw message delivery removes the SNS JSON envelope. The SQS body is then the original message.

DataValue
Retries to SQS100,015 attempts over 23 days (fixed policy) [4]
Subscription DLQOptional, for each subscription. It must be in the same account and region as the subscription. [4]

2.4Another team’s queue

Part ids
e1-other, e2-other
Type
SNS subscription

This queue is a second subscription on the same topic. It gets a full copy of each event. Our queue and this queue do not affect each other.

In the app, this queue becomes empty slowly. The consumers of this queue are not part of the model. Simulation

2.5Cross-region subscription

Part id
cross-fan
Type
SNS → SQS, us-east-1 → us-east-2

This subscription exists only in one-topic mode (Fan-out: 1 topic → both regions). The us-east-1 topic then also sends each event to the us-east-2 queue. SNS supports cross-region delivery to SQS. As a result, both regions process each event.

2.6SQS queue

Part ids
e1-sqs, e2-sqs
Type
Amazon SQS standard queue

The SQS queue keeps each event until a pod deletes it. SQS is pull: it never sends to a pod. A pod asks with ReceiveMessage, an HTTP request, and the answer carries the message. Refer to 1.7. When a pod receives a message, SQS does not delete the message. SQS makes the message invisible for the visibility timeout. If the pod does not delete the message, the message becomes visible again.

Standard queues give at-least-once delivery with best-effort order. In the app, bright cubes are visible messages and dim cubes are messages in flight.

DataValue
DeliveryPull, over HTTPS [26]
ReceiveMessage1 to 10 messages, default 1 [26]. App: 1 Simulation
WaitTimeSeconds0 to 20 s. Above 0 is a long poll [26]
Visibility timeout30 s default, range 0 to 43,200 s [6]. App: 3 s Simulation
Retention4 days default, 14 days maximum [6]
maxReceiveCountApp: 3 Simulation

2.7SQS dead-letter queue

Part ids
e1-sqs-dlq, e2-sqs-dlq
Type
Amazon SQS redrive

SQS moves a message to this queue when the ReceiveCount of the message is more than maxReceiveCount. This occurs when pods receive the message many times and do not delete it.

The DLQ must be in the same account and region as the source queue. It must also be the same queue type.

DataValue
TriggerReceiveCount > maxReceiveCount [6]
Retention adviceSet the DLQ retention to more than the source retention.

2.8SQS consumer pods (KEDA)

Part ids
e1-pods, e2-pods
Type
Kubernetes pods with a KEDA ScaledObject

Each pod pulls from SQS: it asks over HTTP, and the answer carries the message. The pod puts the body into an Avro record and sends the record to Artemis over Core. The pod deletes the SQS message only after Artemis confirms the send.

KEDA uses the aws-sqs-queue scaler. The scaler sets the number of replicas to approximately ceil((visible + in-flight messages) ÷ queueLength).

In the app, an idle pod is grey and a busy pod is light violet. A hollow ring from a pod to SQS is an ask. An idle pod continues to ask. A busy pod does not ask. In the 2D view, click one pod to see its pinned broker and its state.

DataValue
queueLength5 messages for each pod (KEDA default) [9]
ReplicasKEDA default maximum 100 [9]. App: 1 to 10 (maximum 10 to 50 in the panel) Simulation
KEDA polling interval30 s default [9]. App: 2 s Simulation
KEDA cooldown period300 s default [9]. The app has no cooldown. Simulation

2.9PrivateLink interface endpoint

Part ids
e1-pl, e2-pl
Type
AWS PrivateLink

The endpoint is a set of ENIs with private IP addresses in the consumer VPC. Traffic goes privately to the endpoint service in the broker VPC. A Network Load Balancer is in front of the endpoint service.

2.10Network Load Balancer

Part ids
e1-nlb, e2-nlb
Type
Layer 4 (TCP) load balancer

The NLB selects a broker for each TCP connection with a flow hash. The connection stays on that broker for its full life. The NLB does not balance individual messages.

In the app, the colour of each line shows the broker of that connection.

DataValue
Target selectionFlow hash of protocol, source and destination IP address and port, and TCP sequence number [8]
TCP idle timeout350 s default, range 60 to 6000 s [8]
Cross-zone load balancingOff by default [8]

2.11Artemis cluster

Part ids
e1-cluster, e2-cluster
Type
ActiveMQ Artemis cluster

The cluster has three brokers with cluster connections between them. Each broker stores only its own messages. Thus three brokers give more capacity, not three copies.

With message-load-balancing=ON_DEMAND (the default), each message that arrives goes round-robin to the brokers whose queue has consumers. The broker that receives the message is one of these brokers. This rule spreads the messages across the brokers. The NLB spreads only the connections.

2.12Brokers 1, 2 and 3 (each region)

Part ids
e1-b1, e1-b2, e1-b3, e2-b1, e2-b2, e2-b3
Type
Artemis broker

Each broker holds these items:

  • The multicast address of its region and the queue that its region consumes. In us-east-1, this is use1.address → use1.queue. In us-east-2, this is use2.address → use2.queue.
  • The standby copy of the queue of the other region. The mirror fills this queue. Consumers do not read it until the other region fails.
  • A dead-letter address.
  • A mirror connection to the broker with the same number in the other region.

Cubes on top of a broker are its stored messages. Violet cubes are messages of this region. Cyan cubes are mirror copies. In the 2D view, click a broker to see its status, its number of stored messages and its mirror backlog.

2.13Artemis dead-letter queue

Part ids
e1-art-dlq, e2-art-dlq
Type
Artemis dead-letter-address

A consumer rolls back when the API sends a 4xx or a 5xx response:

  • 422: the API always rejects the payload (poison).
  • 500: a transient error.
  • 503: the database cannot do the write (or the read). For example, the writer is down or a promotion is in progress.

Each rollback is a failed delivery. The broker waits for redelivery-delay and then delivers the message again. After max-delivery-attempts failed deliveries, the broker moves the message to the dead-letter-address.

A 422 never succeeds, so the message must go to the dead-letter address. A 5xx usually succeeds on a later attempt. Thus redelivery-delay is important during a database failover. Refer to 3.20.

WARNING

If no dead-letter address is set, Artemis discards the message. The message is then lost.

DataValue
max-delivery-attempts10 default [14]. App: 3 Simulation
redelivery-delay0 default [14]. App: 0 to 5 s, default 0 (slider Artemis redelivery-delay) Simulation
No dead-letter address setThe message is discarded [14]

2.14Artemis consumers ×10

Part ids
e1-consumers, e2-consumers
Type
Fixed replicas (no autoscaler)

Ten consumers compete for the messages on the queue. Each message goes to one consumer. Artemis is push: each consumer opens one Core connection through PrivateLink, the NLB pins the connection to one broker, and the broker sends messages down it. Refer to 1.7.

A consumer does its own work (Consumer work time). Then it sends an HTTP request through the ALB to the API in its region. The consumer keeps the message until the response comes back. A 2xx response causes an ack. A 4xx or 5xx response causes a rollback.

KEDA has an artemis-queue scaler. In this model, the number of replicas is fixed: 10 for each region by default, and 10 to 50 with the Artemis consumer count control under Capacity (per region). In the 2D view, click one consumer to see its pinned broker and its state: idle, processing or standby. A consumer that waits for the API shows teal.

DataValue
DeliveryPush, over Core [27]
consumerWindowSize1 MiB default [27]. App: one message at a time Simulation
Replicas10 for each region (model assumption)
CallsThe API in its region (HTTP)
On 2xxAck: the broker deletes the message
On 4xx or 5xxRollback → redelivery → dead-letter address
Work time before the API callApp: 0.1 to 5 s, default 0.5 s Simulation

2.15PrivateLink endpoint (consumer side)

Part ids
e1-pl-c, e2-pl-c
Type
AWS PrivateLink

The Artemis consumers get to the brokers in the same way as the pods. They use an interface endpoint in their VPC to the endpoint service. The app shows this endpoint on the consumer side to make the diagram clear. If the consumers and the pods are in the same VPC, it can be the same endpoint.

2.16NLB (consumer connections)

Part ids
e1-nlb-c, e2-nlb-c
Type
Same endpoint service, layer 4

This part is the Network Load Balancer of the endpoint service, seen from the consumer side. Usually it is the same NLB that the pods use. The NLB hashes the TCP connection of each consumer to one broker, and the connection stays there. Thus the consumers can have an unequal spread. A broker can also have no consumers.

DataValue
BalancesTCP connections, not messages
StickinessLife of the connection

2.17Mirror connections B1, B2 and B3

Part ids
mirror-1, mirror-2, mirror-3
Type
AMQP broker connection, dual mirror

Each broker mirrors to one broker in the other region: B1 to B1, B2 to B2 and B3 to B3. By default, the mirror copies sends and acks asynchronously. With sync=true, clients wait for the mirror.

When the target is not available, mirror events wait in the queue $ACTIVEMQ_ARTEMIS_MIRROR_<name>. The mirror copies only the messages that arrive after you configure the mirror.

DataValue
DefaultAsync [12]
MirroredSends, acks, queue create and delete [1]
Documentation adviceDo not have active consumers on both sides [1]

Parts of the “Both regions (live)” view: API and Aurora

These parts are after the Artemis consumers. The 2D view shows live data for them when you click them.

2.18API

Part ids
e1-api (us-east-1), e2-api (us-east-2)
Type
Kubernetes Deployment with a HorizontalPodAutoscaler (HPA)

The API receives the HTTP request of each consumer through the ALB. It examines the payload. Then it stores the event in Aurora.

For an event, the API by default only writes. It sends INSERT … ON CONFLICT (event_id) DO NOTHING to the one writer. The us-east-2 API sends this write across the region, because the us-east-2 cluster is read-only. The unique key on event_id makes the write idempotent. If the row is already there, the write does nothing. The app calls this “dedupe at write”.

With Read before write on, the API first reads its local reader (e1-2 or e2-1). If the row is there, the API sends 200 and does not write. The app calls this “dedupe at read”. A reader can be behind the writer. Thus “not found” on a reader does not mean “not stored”.

ResponseMeaningConsumer
200Stored, or already stored (dedupe at read or at write)Ack
422The API always rejects this payload (poison)Rollback; Artemis DLQ after 3 attempts
500Transient error (Transient failures slider)Rollback; a retry usually succeeds
503The database is not available: the writer is down, a promotion is in progress, or (read before write) no instance is up in the regionRollback; a retry after the failover succeeds

The HPA sets replicas = ⌈current × metric ÷ target⌉. Here, the metric is in-flight + queued requests for each pod, and the target is 1. Thus desired pods = ⌈(in flight + queued) ÷ 1⌉, from 1 to 10 (the maximum is 10 to 50 with API max pods (HPA)). New pods are ready after 2 s. For scale-down, the HPA uses the highest recommendation of the last 10 s. It never removes a pod that has requests in flight.

The API also answers the queries of the read path. It gets them from the Query API through the ALB, and reads the reader of its region. These queries take request slots, and the HPA counts them.

The ALB sends each request to the next ready pod (round robin), and does not look at the load on that pod. Each pod does 2 requests at the same time. The other requests wait in the pod (counter API queued).

DataValue
ReplicasSet by you. App: HPA minimum 1, maximum 10 Simulation
HPA sync period15 s default [15]. App: 3 s Simulation
Scale-down stabilization300 s default [15]. App: 10 s Simulation
Scale-up stabilization0 s default: at once [15]
Tolerance10% default [15]. The app has no tolerance. Simulation
Each podApp: 2 requests at the same time; ready 2 s after the HPA adds it Simulation
Work for each requestApp: API time per request, 0.05 to 2 s, default 0.25 s Simulation
ReadsThe local reader (e1-2 or e2-1), for each query of the read path. For a consumer call, only with Read before write on
WritesThe writer (e1-1 if no failover occurred)

In the 2D view, click the API to see Ready pods, Starting, In flight, Queued requests and HPA desired.

2.19Aurora cluster (primary and secondary)

Part ids
e1-aurora (primary cluster, us-east-1), e2-aurora (secondary cluster, us-east-2)
Type
Aurora PostgreSQL Global Database, Serverless v2

Primary cluster (us-east-1). This is the only cluster that accepts writes. The writer e1-1 and the reader e1-2 share one cluster volume (six copies across three Availability Zones). Thus the reader is only milliseconds behind the writer. Nothing is copied: the reader reads the same storage. If the writer fails, Aurora promotes the reader. This is an in-region failover. The cluster endpoint follows the writer. The reader endpoint spreads the reads across the readers.

Secondary cluster (us-east-2). This cluster is a read-only copy of the primary. Global Database storage-level replication keeps it up to date, typically less than 1 s behind. It cannot accept writes. If us-east-1 is lost, Aurora can promote this cluster. This failover loses the rows that did not replicate yet (RPO). A planned switchover first waits until the secondary is up to date. Thus a switchover loses nothing.

DataValue
Instances in a clusterOne writer and up to 15 readers [21] [23]
Reader lag in the primary clusterUsually much less than 100 ms [23]. App: 0.15 s Simulation
In-region failoverTypically less than 60 s, often less than 30 s [21]. App: 3 s Simulation
Secondary regionsUp to 10 for each global database, read-only [18]
Replication lag to the secondaryTypically less than 1 s [18]. App: slider, default 0.8 s Simulation
Promotion of the secondaryFailover (RPO more than 0, RTO minutes) or switchover (RPO 0) [19]. App: failover after 5 s Simulation
CapacityServerless v2, for each instance [16]

In the 2D view, click a cluster to see Writer, Rows on the writer and, after a global failover, Rows lost at failover.

2.20Aurora instance e1-1

Part id
aurora-e1-1
Type
Serverless v2 instance, the writer at the start, us-east-1

This instance is the writer at the start. Each INSERT from the two regions runs on it. Thus it is the busiest instance, and its ACU gauge increases first.

Its role label is live. If it fails (Kill writer), Aurora promotes e1-2 after 3 s. When you restart e1-1, it comes back as a reader. Switchover to e1-1 makes it the writer again.

DataValue
Capacity unitACU: 1 ACU ≈ 2 GiB of memory with matching CPU and network [16]
Range0.5 to 256 ACU; 0 ACU with auto-pause in recent versions [16] [17]. App: 0.5 ACU to the Serverless v2 max capacity slider Simulation
Scaling stepAs small as 0.5 ACU, in seconds [16]
Parallel statementsApp: approximately 2 for each ACU (slots = ACU × 2, rounded), 0.12 s for each statement Simulation

In the 2D view, click an instance to see Role, Status, ACU now, Slots busy, Queued ops, Rows and, for a reader, Rows behind the writer. For the writer, you also see Writes per second: the average of the last 5 simulated seconds.

2.21Aurora instance e1-2

Part id
aurora-e1-2
Type
Serverless v2 instance, reader, us-east-1

This reader gets each row from the shared cluster volume. It is only milliseconds behind the writer. It serves the queries of Center and Cube in us-east-1. With Read before write on, it also serves the reads of us-east-1: “Is this event stored?”.

It is the in-region failover target. If the writer fails, Aurora promotes it. A reader in promotion tier 0 or 1 keeps at least the capacity of the writer, so that it can take over. Readers in tiers 2 to 15 scale on their own load [16]. The app does not model promotion tiers. Simulation

DataValue
Lag behind the writerUsually much less than 100 ms [23]. App: 0.15 s Simulation
Promotion tiers0 (first) to 15 [16] [21]
CapacitySame rules as e1-1

2.22Aurora instance e2-1

Part id
aurora-e2-1
Type
Serverless v2 instance, reader, us-east-2 (read-only cluster)

This reader gets each row by Global Database replication. It serves the queries of Center and Cube in us-east-2. With Read before write on, it also serves the reads of us-east-2. It is behind the writer by the replication lag. Thus a row that the writer stored a moment ago is possibly not on it yet. For this reason, the API cannot remove duplicates with a read only.

It is the global failover target. If us-east-1 is lost and Promote us-east-2 if us-east-1 is lost is on, Aurora promotes it to writer after 5 s. The rows that did not replicate to it are lost.

NOTE

The Kill writer button stops only the writer. To stop this reader, use the console: __sim.killDb('e2-1'). A stopped reader has no effect on the writes of the consumers. The queries of Center and Cube in us-east-2 then get 503, because the region has no instance to read from. With Read before write on, the consumer calls in us-east-2 also get 503.

DataValue
Lag behind the writerTypically less than 1 s [18]. App: Replication lag to us-east-2, default 0.8 s Simulation
WritesNone until promoted [18]
CapacitySame rules as e1-1

2.23Global Database replication

Part id
aurora-repl
Type
Aurora storage-level replication, asynchronous

Aurora copies the changes of the primary to the secondary region at the storage layer. It uses dedicated infrastructure, not the database engine. Thus the writer has almost no extra load.

The replication is asynchronous. A commit completes before the change gets to us-east-2. A failover loses the changes that are still in transit. This loss is the RPO. Aurora PostgreSQL can limit it with rds.global_db_rpo. This parameter stops commits while all secondaries are more behind than the limit.

In the app, the writer sends each committed row to each other instance that is up. A row becomes visible on an instance when its cyan particle arrives.

DataValue
LagTypically less than 1 s [18]. App: slider, default 0.8 s Simulation
MetricAuroraGlobalDBRPOLag [19]
RPO limitrds.global_db_rpo, 20 s minimum, not set by default [19]
SwitchoverWaits for the secondary to be up to date: no data loss [19]

2.24Cross-region write

Part id
cross-write
Type
us-east-2 API → the writer in us-east-1

The secondary cluster is read-only. Thus the us-east-2 API connects to the writer in us-east-1 for each INSERT. Each write pays for a cross-region round trip. In the app, a cross-region SQL operation travels for 0.6 s, and a local one for 0.25 s. Simulation

Connect through the global writer endpoint. It follows the primary after a switchover or a failover, so the connection string does not change. The alternative is write forwarding on the secondary: the us-east-2 reader sends the writes to the writer for you.

The counter cross-region writes counts the rows that an API stored on a writer in the other region. After a global failover, the writer is e2-1, and the us-east-1 API then writes across the region.

DataValue
ReasonOnly the primary cluster accepts writes [18]
EndpointGlobal writer endpoint
Write forwardingAurora PostgreSQL 14.9+, 15.4+, 16+. Off by default. apg_write_forward.consistency_mode default SESSION [20]

Parts of the “Inside Artemis” view

This view is a close-up of the us-east-1 cluster. It shows a fixed example: six pod connections pinned 3 / 2 / 1 and ten consumer connections pinned 5 / 5 / 0. The live simulation does not change this view.

2.25Pods that send Avro (Artemis producers)

Part id
clients
Type
Clients

Each pod opens a long-lived Core connection. The coloured cap shows the broker that the NLB pinned the connection to. In this example, six connections are pinned 3 / 2 / 1.

2.26Interface endpoint (ENIs)

Part id
endpoint
Type
AWS PrivateLink

The ENIs have private IP addresses in the client VPC. Traffic goes to the endpoint service in the broker VPC. The traffic does not go through the internet.

2.27Network Load Balancer (endpoint service)

Part id
nlb
Type
Layer 4, TCP

The NLB selects a broker one time for each TCP connection with a flow hash. Then it sends all bytes of that connection to the same broker. A busy connection and an idle connection have the same weight. The NLB never examines individual messages.

DataValue
Hash inputsProtocol, source and destination IP address and port, TCP sequence number [8]
StickinessLife of the connection [8]
TCP idle timeout350 s default [8]

2.28Pinned TCP connections

Part id
conn-lines
Type
NLB flow → broker

The colour of a line shows its broker: B1 is light blue, B2 is pink and B3 is yellow. Each message from a client goes along its connection to the same broker. A broker with more connections receives more messages. The NLB does not correct this imbalance.

2.29Broker 1, broker 2 and broker 3

Part ids
broker-1, broker-2, broker-3
Type
Artemis broker

Each broker stores only the messages that the cluster routes to it. In this example, broker 1 and broker 2 each have 5 consumer connections.

Broker 3 has no consumer connections. With ON_DEMAND load balancing, broker 3 sends each message that arrives on it over the cluster bridge. The message goes to a broker that has consumers.

2.30Address use1.address (MULTICAST)

Part id
address
Type
Artemis address

Producers send to the address, not to a queue. MULTICAST routing puts a copy of each message in each queue that is bound to the address (publish-subscribe). With one bound queue, that queue gets every message.

2.31Queue use1.queue

Part id
queue
Type
Artemis queue, competing consumers

This queue is bound to the address. The consumers that are connected to this broker compete for its messages. Each message goes to one of them.

2.32Dead-letter address (for example “DLQ”)

Part id
dla
Type
address-settings

Two address settings control this address: dead-letter-address and max-delivery-attempts (default 10). A message that rolls back that number of times moves to the dead-letter address.

WARNING

If no dead-letter address is set, Artemis discards the message.

2.33Mirror queue $ACTIVEMQ_ARTEMIS_MIRROR_<name>

Part id
mirror-q
Type
Mirror store-and-forward queue

The broker first writes each send and each ack to this queue. Then the broker sends them asynchronously to the paired broker in us-east-2. If the target is down, mirror events collect in this queue. The broker sends them when the target is available again.

2.34Cluster connections (core bridges)

Part id
bridges
Type
Artemis cluster

Brokers send messages to each other over internal bridges. With message-load-balancing=ON_DEMAND (default), each message that arrives goes round-robin to the brokers whose queue for that address has consumers. The broker that receives the message is included.

The parameter redistribution-delay controls the movement of stored messages when a queue loses its consumers. The default is −1, which means off.

DataValue
message-load-balancingON_DEMAND (default) [13]
redistribution-delay−1 default, which means never [13]

2.3510 consumers (fixed replicas)

Part id
consumers
Type
Competing consumers

The broker pushes to each consumer over its Core connection. In this example, the NLB pinned the ten consumers to broker 1 and broker 2 (5 + 5). Broker 3 has no consumers. This imbalance is the reason that ON_DEMAND load balancing and redistribution are important.

2.36Interface endpoint (consumer VPC)

Part id
endpoint-c
Type
AWS PrivateLink

The 10 consumers also connect through PrivateLink. Their connections go to the same endpoint service. Thus the same NLB pins each consumer connection to one broker.

2.37NLB (consumer connections)

Part id
nlb-c
Type
Same endpoint service, layer 4

This part is the same Network Load Balancer, shown on the consumer side. It hashed the 10 consumer connections to broker 1 and broker 2 (5 + 5) and none to broker 3. Nothing rebalances these connections until the consumers reconnect.

DataValue
BalancesTCP connections, not messages

2.38us-east-2 brokers (mirror targets)

Part id
remote
Type
Other region

Each us-east-1 broker mirrors to one us-east-2 broker, and the reverse. The copies keep their names. Thus us-east-2 holds use1.address → use1.queue as a standby copy, next to its own use2.queue.

The us-east-2 consumers read only use2.queue. They attach to use1.queue only after a failover. Configure the address and queue of the other region on each broker. Then a copy always has a queue to go to.

Parts of the DLQ lane

These parts are on the outer edge of each region in the “Both regions (live)” view.

2.40DLQ processor

Part ids
e1-art-dlq-proc, e2-art-dlq-proc
Type
Kubernetes Deployment

The processor is a consumer on the dead-letter queue, so the broker pushes to it over Core, as for the other consumers. It takes a message, writes it to S3 with PutObject, and acks it only after S3 sends 200.

Each broker keeps its own dead-lettered messages, and one connection goes to one broker. Thus a real processor must have redistribution-delay on the DLQ address or one connection to each broker. Simulation The app reads from each broker that is up, and it draws the connection as a straight line. A message on a broker that is down stays there until the broker restarts.

2.41S3 bucket dlq-archive

Part ids
e1-s3, e2-s3
Type
Amazon S3 bucket, one for each region

The bucket keeps each message from the Artemis DLQ as one object. You can read a message and send it again later.

The key comes from the message id, for example dlq/artemis/us-east-1/<id>. A second PUT of the same message overwrites the object. It does not make a second object.

S3 is strongly consistent: after a 200, a read gets the new object. Objects stay in their region if you do not set replication. Simulation The two buckets do not replicate. S3 fails only when its region has an outage.

Load balancer in front of the API

This part is between the API and its callers (the consumers and the Query API) in each region, in the “Both regions (live)” view.

2.42Application Load Balancer (ALB)

Part ids
e1-alb, e2-alb
Type
Internal layer 7 (HTTP) load balancer, one for each region

The ALB selects a target for each HTTP request, not for each connection. Thus a keep-alive connection does not tie a caller to one API pod. The NLB is different: it pins a full connection to one broker.

The routing algorithm is round robin, the ALB default. The ALB sends each request to the next healthy pod in the sequence. It does not look at the load on that pod. A slow pod gets its full share, and the extra requests wait in that pod. A new pod gets its full share when it is healthy.

CAUTION

If a pod stops before its deregistration completes, the consumer gets a 5xx response. This rollback uses one delivery attempt of the message. Let pods drain before they stop. If not, a deployment can send healthy messages to the Artemis DLQ.

DataValue
Routing algorithmRound robin (default). Least outstanding requests and weighted random are also available. [25]
Deregistration delay300 s default [25]

Simulation The targets are the pods that are ready. Each pod serves 2 requests at a time, and the other requests wait in the pod. The ALB adds no latency and no errors of its own.

Parts of the read path

These parts are on the outer side of each region, in the “Both regions (live)” view. They only read. Set Show to Ingestion only to hide them. Simulation The diagram puts one of each part in each region. This is a model choice.

2.43Center

Part ids
e1-ui-center, e2-ui-center
Type
UI, a separate deployment. Read only.

Center is one of the two UIs. It is a separate deployment from Cube, so it has its own release and its own failures. It sends queries to the Query API in its region. It does not write.

Simulation The rate is Center queries, 0 to 5 queries/s in each region, default 0.5. At the default rates, each region sends 1 query each second, and Center and Cube take turns. A UI holds a maximum of 20 open queries. Above this limit, it skips the refresh. The label shows the number of rows that the UI saw in its last answer.

2.44Cube (analytics)

Part ids
e1-ui-cube, e2-ui-cube
Type
UI, a separate deployment. Read only.

Cube is the analytics UI. It is a separate deployment from Center, so it has its own release and its own failures. It sends queries to the same Query API. It does not write.

Simulation The rate is Cube queries (analytics), 0 to 3 queries/s in each region, default 0.5. An analytics query takes 4 times the time of a Center query on the reader, and costs 4 times the ACUs. These two ratios are a model choice. They are not measurements.

2.45Query API

Part ids
e1-query-api, e2-query-api
Type
Kubernetes Deployment with an HPA, one for each region. It is between the UIs and the existing API. Read only.

Both UIs get their data from the Query API. The Query API has no database connection. It calls the existing API through the ALB of its region. The existing API then reads the reader of its region.

The Query API scales like the existing API. An HPA sets from 1 pod to the maximum, from the in-flight and queued queries for each pod. A pod holds its request slot until the existing API answers. Thus the maximum also limits the number of reads that get to the existing API at one time.

NOTE

Reads and ingestion use the same pods of the existing API. A query takes a request slot, and the HPA of the existing API counts it. Set the API maximum for both. Scenarios 40 and 41 in 6.3 compare the two.

Simulation Each pod serves 2 queries at a time, and the other queries wait in the pod. A new pod is ready after 2 s. Each query goes to the next ready pod. If no reader is up in the region, the API reads the writer if the writer is in that region. If no instance is up, the query gets a 503 response.

3Operation

This section tells how the system operates. Each subsection agrees with one step of the guided tour in the app. Each subsection also tells what the tour step does to the simulation.

NOTE

A tour step can change simulation settings. When the step ends, the app sets these settings back to their values from before the step.

3.1One event, end to end (tour step 1)

An event leaves the producer and goes to the SNS topic of its region. SNS puts the event in the SQS queue of that region. A pod converts the event to Avro. The pod sends the Avro message through PrivateLink to one of three Artemis brokers. One of 10 consumers takes the message and calls the API of its region. The API stores the event in Aurora PostgreSQL: one writer in us-east-1, and a reader in each region.

Use the colours to follow the message. Amber is the event. Violet is the Avro message. Teal is the HTTP call to the API. Light blue is SQL to Aurora. Cyan is a copy between the regions. Green is a processed message.

End-to-end message path in 13 stages Stage 1 publish, 2 SNS fan-out, 3 SQS stores, 4 pod receives and the message becomes invisible, 5 pod converts to Avro, 6 pod sends through PrivateLink and the NLB to one broker, 7 broker confirms and the pod deletes the SQS message, 8 address to queue and mirror copy, 9 a consumer receives, 10 the consumer calls the API over HTTP, 11 the API writes INSERT ON CONFLICT on the one writer, 12 the API sends 2xx after the commit, 13 the consumer acks and the ack is mirrored. The writer replicates each new row to the readers asynchronously. A 4xx or 5xx at stage 12 causes a rollback. 1 · PublishThe producer callsSNS Publish 2 · Fan-outSNS sends one copyto each subscription 3 · StoreSQS keeps the messageuntil a delete 4 · ReceiveA pod receives it;it becomes invisible 5 · ConvertThe pod puts the bodyin an Avro record 6 · SendPrivateLink → NLB →one pinned broker 7 · Confirm, deleteThe broker confirms;the pod deletes in SQS 8 · Route, mirroraddress → queue;copy to other region 9 · DeliverOne of 10 consumersreceives it 10 · Call APIThe consumer sendsHTTP to an API pod 11 · WriteINSERT … ON CONFLICTon the one writer 12 · AnswerThe API sends 2xxafter the commit 13 · DoneThe consumer acks;the ack is mirrored Cyan: the writer copies each new rowto e1-2 and e2-1 (async replication). 4xx or 5xx at stage 12: rollback,redelivery, then the dead-letter address.
Figure 2. End-to-end message path. The colour changes from amber (event) to violet (Avro message) to teal (HTTP request) to light blue (SQL write) to green (processed).

Simulation in this step: publish rate 1.2 events/s, clock not paused.

3.2SNS fan-out: each subscription gets a copy (tour step 2)

SNS does not give an event to only one queue. SNS sends a copy to each subscription. Our queue and the queue of the other team each get one copy. The two copies do not affect each other.

SNS retries delivery to SQS: 100,015 attempts over 23 days. A subscription can also have its own DLQ for deliveries that fail.

SNS can retry only while it operates. A region outage also stops the SNS topic in that region. Simulation A publish that is in transit gets an error, and the producer tries again. A message that SNS accepted before the outage waits, and SNS delivers it when the region is available again.

SNS fan-out to two subscriptions The producer publishes one event to the SNS topic. The topic sends copy 1 to the queue of another team and copy 2 to our SQS queue. Deliveries that fail after all retries can go to an optional subscription DLQ. Producerpublishes 1 event SNS topicone publish in Copy 2: our queueSQS, this pipeline Subscription DLQoptional Copy 1: other teamseparate subscription Retries to SQS: 100,015 attempts over 23 days copies do not affect each other
Figure 3. SNS fan-out. One publish gives one copy for each subscription. A failed delivery goes to the subscription DLQ only after all retries.

Simulation in this step: publish rate 0.8 events/s.

3.3One topic or a topic per region (tour step 3)

One topic in us-east-1 can have subscribed queues in the two regions. SNS supports cross-region delivery to SQS. Then each event goes to us-east-1 and to us-east-2. One event then gives two Avro messages.

With a topic in each region, the producer publishes to the topic of its own region. Each event then goes to one region. Look at the counter deliveries / event in the Simulate panel. Use the Fan-out switch to compare the two modes.

WARNING

One topic with queues in the two regions causes two times the processing for each event. Make sure that this is the behaviour that you want.

Topic per region compared with one topic for both regions Left: the producer publishes each event to the topic of one region, and that region's queue gets one copy. The result is 1.00 deliveries per event. Right: the producer publishes to the us-east-1 topic, which sends to the queues in both regions. The result is 2.00 deliveries per event. Topic per region (default) 1 topic → both regions Producer SNS us-east-1 SNS us-east-2 SQS us-east-1 SQS us-east-2 1 copy for each event → 1.00 deliveries / event Producer SNS us-east-1 SQS us-east-1 SQS us-east-2 cross-region 2 copies for each event → 2.00 deliveries / event
Figure 4. Fan-out topology. The counts are the results of scenarios 1 and 2 in 6.3.

Simulation in this step: Fan-out set to 1 topic → both regions, publish rate 1.2 events/s.

3.4Receive, convert, send, then delete (tour step 4)

A pod pulls a message: it asks SQS with ReceiveMessage, an HTTP request, and the answer carries the message. In the app, the hollow ring is the ask. SQS does not delete the message. The message becomes invisible for the visibility timeout. In the app, the cube of the message becomes dim.

The pod wraps the SQS body in an Avro record. Avro is a compact binary format without field names. Thus a reader needs the schema of the writer, usually from a schema registry. In the Confluent wire format, a record starts with a 0x00 magic byte and a 4-byte schema id.

The pod sends the record to Artemis over Core. The pod deletes the SQS message only after the broker confirms the send. If the pod stops between the send and the delete, the message becomes visible again. A pod then sends it again. For this reason, the pipeline is at-least-once.

States of an SQS message: visibility, delete and redrive A visible message goes to in flight when a pod receives it, and its ReceiveCount increases by one. From in flight, the message is deleted when the pod deletes it after the broker confirms. If the visibility timeout ends with no delete, the message is visible again. When a receive finds ReceiveCount more than maxReceiveCount, SQS moves the message to the SQS DLQ. Visiblea pod can receive it In flightinvisible for thevisibility timeout SQS DLQredrive DeletedDeleteMessage ReceiveMessage ReceiveCount + 1 next receive with ReceiveCount > maxReceiveCount (app: 3) visibility timeout ends with no DeleteMessage (app: 3 s) after the broker confirms the send
Figure 5. SQS message states. The delete is the commit. A crash before the delete causes a second send to Artemis.

Simulation in this step: publish rate 2.5 events/s.

3.5KEDA adds pods when the queue grows (tour step 5)

The aws-sqs-queue scaler counts the visible and the in-flight messages. It targets queueLength messages for each pod. The default queueLength is 5. For example, 30 messages give 6 pods.

In this step, a burst of 30 events arrives. The pod grid grows. Then the grid becomes smaller when the queue is empty.

NOTE

Real KEDA polls every 30 s by default. The simulation polls every 2 s so that you can see the change. The simulation keeps the number of pods between 1 and 10. Simulation

KEDA replicas as a function of SQS messages A step chart. The number of pods is the number of visible and in-flight messages divided by 5, rounded up, with a minimum of 1 and a maximum of 10 in the app. 30 messages give 6 pods. 50 or more messages give 10 pods. 0102030405060 0246810 messages in SQS (visible + in flight) pods (replicas) 30 messages → 6 pods minimum 1 maximum 10 (app)
Figure 6. KEDA aws-sqs-queue scaler: pods = ceil(messages ÷ 5), from 1 to 10 in the app. The KEDA default maximum is 100 replicas.

Simulation in this step: publish rate 0.6 events/s and one burst of 30 events.

3.6The NLB balances connections, not messages (tour step 6)

PrivateLink puts a Network Load Balancer in front of the brokers. The NLB hashes each new TCP connection to one broker. The connection stays on that broker for its full life.

In the example, six pods have connections pinned 3 / 2 / 1. Thus half of all messages arrive at broker 1. The consumers also connect through PrivateLink, and the NLB pins them in the same way: 5 / 5 / 0. Broker 3 has no consumers.

The NLB never divides the messages of one connection. An even spread of messages across the brokers comes from the Artemis cluster (3.8).

NLB connection pins for pods and consumers Six pod connections go through the NLB. Three are pinned to broker 1, two to broker 2 and one to broker 3. Ten consumer connections go through the NLB on the consumer side. Five are pinned to broker 1, five to broker 2 and none to broker 3. Pod 1 Pod 2 Pod 3 Pod 4 Pod 5 Pod 6 NLBflow hash for each TCP connection Broker 13 pod connections5 consumers Broker 22 pod connections5 consumers Broker 31 pod connection0 consumers NLBconsumer side Consumer 1 Consumer 2 Consumer 3 Consumer 4 Consumer 5 Consumer 6 Consumer 7 Consumer 8 Consumer 9 Consumer 10 pod connections: 3 / 2 / 1 consumer connections: 5 / 5 / 0
Figure 7. NLB connection pins in the “Inside Artemis” example. The line colour is the broker colour: B1 light blue, B2 pink, B3 yellow.

Simulation in this step: the “Inside Artemis” view shows six pod connections in the colours of their brokers.

3.7Address → queue → one consumer (tour step 7)

Pods in us-east-1 send to the address use1.address, not to a queue. The routing type of the address is MULTICAST. Thus Artemis puts a copy of each message in each queue that is bound to the address.

Only use1.queue is bound, so it gets every message. us-east-2 has its own use2.address → use2.queue. The consumers on use1.queue compete, and each message goes to one consumer. The broker pushes the message over the Core connection of the consumer, with no ask for each message. In step 4, a pod asks each time. Refer to 1.7.

Multicast address, bound queue and competing consumers Pods send Avro messages to use1.address, which has the routing type MULTICAST. The address puts a copy in each bound queue. Only use1.queue is bound. Five competing consumers are shown; one consumer receives each message. Podssend Avro use1.addressMULTICASTcopy to each bound queue use1.queuethe one bound queue Consumer Consumer Consumer (gets it) Consumer Consumer competing consumers: one consumer gets each message
Figure 8. MULTICAST address and queue. Each bound queue gets a full copy. Here only one queue is bound.

Simulation in this step: an animated Avro message goes from a pod to use1.address, then to use1.queue, then to one consumer.

3.8The cluster spreads messages across brokers (tour step 8)

Each broker stores only its own messages. Three brokers give more capacity, not three copies.

The parameter message-load-balancing=ON_DEMAND is the default. With it, a broker sends each message that arrives round-robin to each broker whose queue has consumers. The broker that receives the message is included. This rule, not the NLB, spreads the messages equally.

In the example, the NLB pinned all consumers to broker 1 and broker 2. Thus messages alternate between broker 1 and broker 2. A message that arrives on broker 3 always crosses a bridge, which adds one hop.

A stored message on a broker that loses its consumers moves only if redistribution-delay is 0 or more. The default is −1, which means never.

ON_DEMAND round-robin across brokers that have consumers A pod connection is pinned to broker 3, which has no consumers. Each message that arrives on broker 3 goes over a core bridge, round-robin, to broker 1 or broker 2. Broker 1 and broker 2 each have five consumers. Podpinned to B3 Broker 1has consumers Broker 2has consumers Broker 3no consumers arrives (NLB pin) core bridge, ON_DEMAND messages 1, 3, 5 … messages 2, 4, 6 … 5 consumerspinned to B1 5 consumerspinned to B2 0 consumersnothing on B3 Stored messages leave a broker with no consumers only if redistribution-delay ≥ 0 (default −1).
Figure 9. Artemis ON_DEMAND load balancing. Compare it with Figure 7: the NLB sets where messages arrive, and the cluster sets where messages are stored.

Simulation in this step: an animated message arrives on broker 3, crosses the bridge to broker 1 and goes to a consumer.

3.9Mirror: an async standby copy in the other region (tour step 9)

Broker 1 writes each send to its mirror queue. Then it sends the event asynchronously to broker 1 in us-east-2. The copy keeps its names. It goes into use1.address → use1.queue on us-east-2, next to use2.queue. No consumer reads it there.

When a consumer in us-east-1 acks the message, the broker also mirrors the ack. The ack removes the copy in us-east-2.

WARNING

The send of the client completes before the copy gets to us-east-2. If us-east-1 fails in this gap, us-east-2 does not have the message. The message stays in us-east-1 until recovery.

With sync=true, clients wait for the mirror. This adds latency. It also blocks clients while the mirror is disconnected.

Sequence of a mirrored send and a mirrored ack 1 the pod sends Avro to broker 1 in us-east-1. 2 broker 1 writes a mirror event. 3 broker 1 tells the pod that the send is complete. A gap follows in which us-east-2 has no copy. 4 the mirror sends the copy to broker 1 in us-east-2, into use1.queue. 5 broker 1 delivers the message to a consumer. 6 the consumer acks. 7 broker 1 writes the ack to the mirror. 8 the ack arrives in us-east-2 and deletes the copy. Podclient Consumerus-east-1 Broker 1us-east-1 Mirror queueon broker 1 Broker 1us-east-2, use1.queue Gap: the send is complete, but us-east-2 has no copy yet. If us-east-1 fails now, the message stays on its disk until recovery. 1 · send Avro 2 · write mirror event 3 · send complete 4 · async copy→ use1.queue (standby) 5 · deliver 6 · ack 7 · mirror the ack 8 · ack: copy deleted
Figure 10. Mirror of a send and of its ack. The order of the stages after stage 3 depends on the mirror lag. The red band is the async gap.

Simulation in this step: the animation shows the send, the copy to us-east-2 and the mirrored ack that deletes the copy. Use the Mirror lag slider to change the delay of the mirror (default 0.7 s Simulation).

NOTE

To see the pipeline with no copies, set Mirrored queues on to off in the Simulate panel. The tour keeps it on. Scenarios 44 to 51 in 6.3 show what changes.

3.10Per-region queue names keep the copies on standby (tour step 10)

Each region consumes only its own queue. us-east-1 reads use1.queue, and us-east-2 reads use2.queue. The cyan copies stay in the queue of the other region, and nobody reads them. The mirrored ack deletes them. The duplicates counter stays at 0.

Per-region names: own queue and standby copy in each region In us-east-1, consumers read use1.queue. The broker also holds use2.queue, a standby copy that nobody reads. In us-east-2, consumers read use2.queue, and use1.queue is the standby copy. The mirror sends each region's queue to the standby copy in the other region. Consumers attach to the standby copy only on failover. mirror: copy + ack (async) us-east-1 us-east-2 Consumersus-east-1 use1.address → use1.queueown traffic, consumed use2.queue (copy)standby, nobody reads it use2.address → use2.queueown traffic, consumed use1.queue (copy)standby, nobody reads it Consumersus-east-2 failover only failover only
Figure 11. Per-region names (use1.*, use2.*). The copy keeps the name of its source region. Thus it lands in a queue that the local consumers do not read.

The names are the key. Suppose that the two regions use one address and queue name. Then each copy goes into the queue that the consumers of the other region read. An idle consumer takes the copy before the ack arrives. Each message is then processed two times.

WARNING

Do not use one queue name with active consumers in the two regions. The mirror then causes duplicate processing. The Artemis documentation also tells you not to have active consumers on both sides.

Duplicate race with one shared queue name Both regions use events.queue. 1 us-east-1 stores message M. 2 the mirror copy lands in events.queue in us-east-2. 3 a consumer in us-east-1 processes M. 4 an idle consumer in us-east-2 takes the copy, so M is processed two times. 5 the mirrored ack arrives in us-east-2 too late, because the copy is already gone. Both regions use one queue name: events.queue us-east-1 us-east-2 1 · us-east-1 stores message M 3 · a consumer processes M 2 · copy lands in events.queue 4 · an idle consumer takes the copy→ M is processed two times 5 · the ack arrivesthe copy is already gone time
Figure 12. The duplicate race with a shared name. With per-region names, the copy of stage 2 waits in use1.queue, and stage 4 does not occur.

To see the race, set Artemis consumers to Same queue name, both regions consume.

Simulation in this step: Fan-out Topic per region, Artemis consumers Per-region queues, publish rate 2 events/s.

3.11A message that fails each time goes to the SQS DLQ (tour step 11)

In this step, 20% of events have a payload that the pod cannot convert to Avro. These events show red-orange. The step also sends one such event at the start.

A pod that fails does not call DeleteMessage. Thus the message becomes visible again after the visibility timeout, and a pod receives it again. When ReceiveCount is more than maxReceiveCount, SQS moves the message to the DLQ. The value of maxReceiveCount is 3 in the app. Refer to Figure 5 for the redrive path.

WARNING

Set the visibility timeout to more than the worst-case time of a pod. This time includes the Avro conversion and the Artemis send. If the timeout is too short, healthy messages become visible again, and pods receive them two times.

Simulation in this step:

  • Poison: Avro can’t be built: 20%
  • Publish rate: 1.5 events/s
  • Fan-out: Topic per region
  • One poison event (Send 1 poison → SQS DLQ)

3.12Downstream failures: redelivery, then the dead-letter address (tour step 12)

In this step, 20% of events are events that the API always rejects with 422. These events show red-orange in the brokers. The step also sends one such event at the start.

Each 4xx or 5xx response is a rollback, and the broker delivers the message again. After max-delivery-attempts, the broker moves the message to the dead-letter address. The default is 10. The value in the app is 3.

WARNING

Examine your address-settings. If no dead-letter-address is set, Artemis discards the message.

Artemis redelivery and dead-letter address A consumer receives a message from use1.queue. On success, the consumer acks and the message is processed. On failure, if the number of attempts is less than max-delivery-attempts, the consumer rolls back and the broker delivers the message again. When the attempts reach the limit, the message goes to the Artemis DLQ if a dead-letter address is set. If no dead-letter address is set, Artemis discards the message. use1.queuemessage waits Consumerreceives it Processedack success limitreached? max-delivery-attempts(default 10; app: 3) 4xx /5xx no: rollback → the broker delivers it again dead-letteraddress set? yes ArtemisDLQ Discardedmessage lost yes no
Figure 13. Artemis redelivery. A poison message (API 422) fails on each attempt and reaches the limit. A transient failure (API 500) or a database failure (API 503) usually succeeds on a later attempt. The broker waits for redelivery-delay before each new delivery.

Simulation in this step:

  • DLQ processor on: off, so that the messages stay in the DLQ
  • Poison: API always rejects (422): 20%
  • Publish rate: 1.5 events/s
  • Fan-out: Topic per region
  • Artemis consumers: Per-region queues
  • One poison event (Send 1 poison → Artemis DLQ)

3.13A broker fails: connections move, stored messages stay (tour step 13)

Broker 1 in us-east-1 stops. Its clients reconnect, and the NLB pins them to the brokers that are still up. The connection lines move.

The messages that broker 1 already stores stay on it until it starts again. Their mirror copies in us-east-2 are the redundancy. Mirror events for broker 1 wait in a queue. They go when broker 1 is back.

When broker 1 starts again, the current connections stay where they are. Thus broker 1 gets no consumers until the clients reconnect or redistribution is on. Use the Chaos buttons to do this yourself.

Broker failure: connections before and after broker 1 stops Before: NLB connections go to brokers 1, 2 and 3. After broker 1 stops: the clients reconnect and the NLB pins them to brokers 2 and 3 only. The stored messages on broker 1 are stranded. Mirror events for broker 1 wait until it is back. Before: broker 1 is up After: broker 1 is down NLB Broker 1stores messages Broker 2stores messages Broker 3stores messages Each connection stays on its broker. NLB Broker 1 DOWNstored messages stranded Broker 2gets moved clients Broker 3gets moved clients Reconnected clients are pinned to broker 2 and broker 3. Mirror events for broker 1 wait until broker 1 is back.
Figure 14. Broker failure. After a restart, broker 1 has no consumers until the clients reconnect or redistribution is on.

Simulation in this step: publish rate 2.5 events/s, Fan-out Topic per region, Artemis consumers Per-region queues. The step stops broker 1 in us-east-1. When the step ends, the app restarts broker 1 and reconnects the clients.

3.14us-east-1 fails: us-east-2 takes over the standby copy (tour step 14)

us-east-1 is down. The producer publishes to the topic in us-east-2. The us-east-2 consumers also attach to use1.queue, the standby copy. They process the messages that us-east-1 did not complete. The event log shows these changes. The database also fails over. 3.20 tells about that part.

An async mirror has two edge conditions:

  1. A message that us-east-1 accepted just before the failure possibly has no copy yet. The message stays on the disk of us-east-1 until recovery.
  2. us-east-1 processed a message, but the ack did not get to us-east-2. Then us-east-2 processes the message again.
WARNING

A failover can cause duplicate processing. Make sure that the API removes duplicates with the event id (refer to 3.18).

When this step ends, us-east-1 starts again, and the consumers fail back. A switchover then moves the Aurora writer back to e1-1.

Region failover and failback timeline Phase 1 normal: the producer publishes to the topic of the event's region and us-east-2 consumers read use2.queue only. Phase 2 us-east-1 outage: the producer publishes all events to the us-east-2 topic, us-east-2 consumers also attach to use1.queue, and two edge conditions can occur. Phase 3 restored: us-east-2 consumers detach from use1.queue, and the mirror backlog drains. 1 · Normal 2 · us-east-1 outage 3 · us-east-1 restored Producer us-east-2consumers Edgeconditions Publishes to the topic ofthe region of the event Publishes all events tothe us-east-2 topic Publishes to the twotopics again Read use2.queue onlyuse1.queue copy: standby Also attach to use1.queue(the standby copy) Detach from use1.queue(failback) Copies and acks crossin the two directions A · not mirrored yet → stayson the us-east-1 disk B · ack not mirrored → processedagain → dedupe on event id Queued mirror events goto the other region us-east-1 consumesuse1.queue again
Figure 15. Region failover and failback with per-region names. Scenario 11 in 6.3 shows one duplicate at the switch (edge condition B).

Simulation in this step: publish rate 2 events/s, Fan-out Topic per region, Artemis consumers Per-region queues, Artemis redelivery-delay 2 s. After 1.5 s, the step starts a us-east-1 region outage. When the step ends, the app ends the outage and starts a switchover to e1-1.

3.15Consumers call the API; the ack waits for its answer (tour step 15)

A consumer does not complete a message alone. After its own work, it sends an HTTP request (teal) through the ALB to an API pod in its region. The ALB selects the next pod with round robin, for each request. The consumer keeps the message until the response comes back.

  • 2xx (green): the consumer acks, and Artemis deletes the message.
  • 4xx or 5xx (red): the consumer rolls back. The broker delivers the message again. After max-delivery-attempts (3 here), the broker moves it to the dead-letter address.

In this step, one event that the API always rejects with 422 is sent at the start. Follow it into the Artemis DLQ. 2.18 gives all API responses.

WARNING

Send the ack only after a 2xx response. If the consumer acks before the API answers, a failed write loses the event.

Sequence of one API request 1 the broker delivers the message to a consumer. 2 the consumer sends an HTTP request to an API pod. 3 the API pod examines the payload: a poison payload gives 422 and a transient error gives 500. 4 only with Read before write on, the API reads the local reader; if the row is there, the API sends 200 and does not write. 5 the API sends INSERT ON CONFLICT (event_id) DO NOTHING to the writer. 6 the writer inserts the row, or does nothing if the row is already there (dedupe at write). 7 the writer replicates the new row to the other instances asynchronously. 8 the API sends 200 to the consumer. 9 the consumer acks, and the broker mirrors the ack. On a 4xx or 5xx, the consumer rolls back and the broker delivers the message again after redelivery-delay. Brokeruse1.queue Consumersame region API podHPA 1–10 pods Local readere1-2 or e2-1 Writere1-1 Otherinstances 1 · deliver 2 · HTTP request 3 · poison → 422; transient → 500 4 · read (option)row there → 200, no write 5 · INSERT … ON CONFLICT (event_id) DO NOTHING 6 · inserted, or row there: dedupe at write 7 · async replication 8 · 200 9 · ack (mirrored) 4xx or 5xx at stage 8: the consumer rolls back and does not ack. The broker delivers the message again after redelivery-delay. After 3 failed attempts (app), it goes to the dead-letter address.
Figure 16. One API request. Stage 4 occurs only with Read before write on. Stage 7 continues after stage 8: the response does not wait for the replication.

Simulation in this step:

  • Publish rate: 1.5 events/s
  • Fan-out: Topic per region
  • Artemis consumers: Per-region queues
  • One poison event (Send 1 poison → Artemis DLQ)

3.16The HPA adds API pods when requests collect (tour step 16)

The API starts with 1 pod in each region. At each poll, the HorizontalPodAutoscaler calculates desired = ⌈current × metric ÷ target⌉. Here, the metric is in-flight + queued requests for each pod, and the target is 1. Thus 6 waiting requests give 6 pods (maximum 10).

In this step, a burst of 40 events arrives, and each request takes 0.6 s. The API grid grows. A new pod is ready 2 s after the HPA adds it. Scale-down occurs only when the demand stays lower for 10 s. The two regions scale independently.

NOTE

Real HPA syncs every 15 s and waits 300 s before it scales down. The simulation uses 3 s and 10 s so that you can see the change. Simulation

NOTE

More API pods send more statements to the one writer. Set maxReplicas to a value that the database can serve.

HPA replicas as a function of API requests A step chart. The number of API pods is the number of in-flight and queued requests divided by the target 1, rounded up, with a minimum of 1 and a maximum of 10. 6 requests give 6 pods. 10 or more requests give 10 pods. Scale-up occurs at once. Scale-down occurs after 10 s of lower demand. 024681012 0246810 API requests in flight + queued (one region) API pods (replicas) 6 requests → 6 pods minimum 1 maximum 10 up: at once (pod ready after 2 s) down: after 10 s of lower demand
Figure 17. HPA rule in the app: pods = ⌈(in flight + queued) ÷ 1⌉, from 1 to 10. Compare it with the KEDA rule in Figure 6.

Simulation in this step: publish rate 1 event/s, API time per request 0.6 s, Fan-out Topic per region, Artemis consumers Per-region queues, and one burst of 40 events.

3.17One writer for the two regions (tour step 17)

The database is an Aurora PostgreSQL Global Database. The primary cluster in us-east-1 has the writer e1-1 and the reader e1-2. The secondary cluster in us-east-2 has one reader, e2-1. The secondary cluster is read-only.

Each API pod writes to the one writer (light blue): INSERT … ON CONFLICT (event_id) DO NOTHING. The readers get each row by replication (cyan). They serve the reads of their region. Thus each us-east-2 write crosses to us-east-1 and pays for a round trip. Look at the counter cross-region writes in the Simulate panel.

The alternative in Aurora is write forwarding. The us-east-2 reader then sends the writes to the writer for you. The app does not model write forwarding. Simulation

Refer to Figure 1 and to the table in 1.4 for the paths. 2.24 gives the data for the cross-region write.

Simulation in this step: publish rate 2 events/s, Fan-out Topic per region, Artemis consumers Per-region queues.

3.18Replica lag: the read can miss, the unique key cannot (tour step 18)

In this step, the two regions consume the same queue name (as in 3.10). Thus most events arrive two times, one time in each region. The replication to us-east-2 is now 3 s behind (usually less than 1 s). The step also sets Read before write to on. This switch is off by default.

The API first reads its local reader. If the row is there, the API sends 200 and does not write. This is dedupe at read. But e2-1 is behind the writer. Thus “not found” does not mean “not stored”.

The write catches the remaining duplicates: INSERT … ON CONFLICT (event_id) DO NOTHING on the writer. This is dedupe at write. The counter rows stored stays equal to the unique events. The read is only a shortcut. The unique key makes the API idempotent.

WARNING

Do not remove duplicates with a read on a replica only. A replica can be behind the writer, and two deliveries in different regions can both miss the row. Put a unique key on event_id and use ON CONFLICT DO NOTHING on the writer.

Dedupe at read and dedupe at write An API request with an event id arrives. If Read before write is on, the API reads its local reader. If the row is on the reader, the API sends 200 without a write: dedupe at read. If Read before write is off, or the row is not on the reader, the API sends INSERT ON CONFLICT (event_id) DO NOTHING to the writer. If the row is already on the writer, nothing is inserted and the API sends 200: dedupe at write. If not, the writer inserts the row, the API sends 200, and the row replicates to the readers. API requestevent id Read beforewrite on? Read local readere1-2 or e2-1 yes 200, no writededupe at read row there not found no (default) INSERT … ONCONFLICT DO NOTHING 200dedupe at write Row inserted, 200then replication row on the writer new row Only the writer sees everycommitted row. The unique keydecides, not the read.
Figure 18. Dedupe in the API. With Read before write off (default), each request goes straight to the write. A reader that is behind sends a duplicate on to the write, and the write removes it.

Scenario 17 in 6.3 shows the result: 120 events, 120 duplicate deliveries, 120 rows. 35 duplicates stop at the read and 85 stop at the write.

Simulation in this step: publish rate 2 events/s, Fan-out Topic per region, Artemis consumers Same queue name, both regions consume, Replication lag to us-east-2 3 s, Read before write on.

3.19Serverless v2: capacity follows the load in 0.5-ACU steps (tour step 19)

Each Aurora instance has its own capacity in ACUs. 1 ACU is approximately 2 GiB of memory with matching CPU and network. Aurora scales each instance between your minimum and your maximum. The steps are as small as 0.5 ACU, and they occur in seconds. Aurora does not drop connections when it scales.

In the app, the range is 0.5 to 8 ACU by default. Serverless v2 max capacity can set the maximum to 32 ACU. An instance runs approximately 2 statements at the same time for each ACU. The real range is up to 256 ACU, and down to 0 with auto-pause in recent versions.

In this step, the rate is 8 events/s. The ACU gauge of the writer increases from 0.5, because each write from the two regions goes to it. The readers stay small. If an instance gets to its maximum, its statements wait and become slower. Each API request then takes more time. To see this, set Serverless v2 max capacity to 1 ACU.

The tour hides the read path in this step. With the read path on, the readers do not stay small: the queries of Center and Cube add load on them, and a Cube query costs 4 times a Center query.

The ACUs of an instance set two things in the app: the number of statements that it runs at the same time, and how much each statement slows down at high load. The app does not model the cost of an ACU, the memory cache or the connection limit.

App rule (each 1 s)Value
Cost of one statementWrite 0.45 ACU-seconds (ACU_WRITE), read 0.15 ACU-seconds (ACU_READ)
Utilization(cost of the statements that started in the last second + queued statements × 0.15) ÷ current ACU
Scale upUtilization more than 70% (ACU_UP): ACU + 50%, minimum + 0.5, rounded to 0.5, up to the maximum
Scale downUtilization less than 30% (ACU_DOWN) for 3 polls: − 0.5 ACU for each poll, down to 0.5
Parallel slotsACU × 2, rounded, minimum 1
Statement time0.12 s (DB_OP) × utilization, from 1 to 4 times (DB_SLOW)
NOTE

The app rule is a simple model. Real Serverless v2 scales on CPU, memory and network load. A reader in promotion tier 0 or 1 also keeps at least the capacity of the writer. Simulation

The event log shows a change of ACU only when the integer part changes, for example “Aurora e1-1 (writer): 1.5 → 2.5 ACU (utilisation 96%)”. The counter ACU e1-1 / e1-2 / e2-1 shows the current values.

Simulation in this step: publish rate 8 events/s, Fan-out Topic per region, Artemis consumers Per-region queues.

3.20us-east-1 fails and takes the writer with it (tour step 20)

us-east-1 is down again. This time e1-1 and e1-2 also stop. The us-east-2 API cannot write: each INSERT fails with 503. Thus the us-east-2 consumers roll back and try again.

After 5 s, Aurora promotes the secondary: e2-1 becomes the writer. This is a global failover. A real global failover takes minutes. The redelivery delay is 2 s here, so the retries continue for longer than the promotion. At 0 s, the retries use all 3 attempts, and the messages go to the dead-letter address.

The replication lag is 2 s here. Thus the rows that the old writer committed in its last 2 s did not get to us-east-2. These rows are lost (counter rows lost (RPO)), unless their event is delivered again. This is the RPO. A planned switchover waits until the secondary is up to date, and loses nothing. Use a switchover to fail back.

When this step ends, us-east-1 comes back. Its instances come back as readers. Then a switchover moves the writer back to e1-1.

WARNING

A global failover loses the committed rows that did not replicate yet. The consumer possibly acked these events already. Monitor AuroraGlobalDBRPOLag. Think about rds.global_db_rpo to limit the loss.

WARNING

With redelivery-delay 0, three fast 503 responses during a failover send a good message to the dead-letter address. Set redelivery-delay so that the retries continue for longer than a failover.

Database failover and failback timeline Phase 1 normal: the writer is e1-1 and the us-east-2 API writes across the region. Phase 2 us-east-1 outage: e1-1 and e1-2 stop, writes get 503 for 5 s while e2-1 is promoted, and the consumers roll back and retry after redelivery-delay. Phase 3 e2-1 is the writer: rows that did not replicate are lost (RPO) and us-east-2 writes are now local. Phase 4 us-east-1 restored: e1-1 and e1-2 come back as readers, and a switchover moves the writer back to e1-1 after 3 s with no data loss. 1 · Normal 2 · Outage, promotion 3 · e2-1 is the writer 4 · Restored, switchover Writer us-east-2API Rows e1-1 (us-east-1)e1-2, e2-1: readers none: e1-1 and e1-2 downe2-1 promoting (5 s) e2-1 (us-east-2)global failover done e1-1, e1-2 rejoin as readers;switchover → e1-1 (3 s) Writes cross to e1-1(cross-region writes) Each INSERT gets 503;roll back, retry afterredelivery-delay Writes are local now;us-east-1 is down Writes cross to e1-1again Replicate to e2-1with the lag Rows in transit toe2-1 at the outage Not on e2-1 → lost(rows lost: RPO) Switchover waits for sync:no data loss
Figure 19. Database failover and failback. A switchover in phase 4 loses nothing. The rows lost in phase 3 stay lost unless their events are delivered again.
EventHow to start it in the appNew writerTime (app / real)Rows lost
In-region failoverKill writerThe other instance in the cluster of the writer (e1-2)3 s / typically less than 60 sNone (shared cluster storage)
Writer down, no replica in its clusterConsole: __sim.killDb('e1-2'), then __sim.killDb('e1-1')None. e1-1 stays the writer, and writes continue when you restart it.—None, but each write gets 503
Global failoverRegion outage (us-east-1) with Promote us-east-2 if us-east-1 is lost one2-15 s / minutesThe rows that did not replicate (RPO)
Region outage, no global failoverRegion outage (us-east-1) with the switch offNone until us-east-1 is back—None, but each write gets 503
SwitchoverSwitchover to e1-1e1-13 s / minutesNone (it waits for sync)

Simulation in this step: publish rate 3 events/s, Fan-out Topic per region, Artemis consumers Per-region queues, Promote us-east-2 if us-east-1 is lost on, Artemis redelivery-delay 2 s, Replication lag to us-east-2 2 s. The step waits until the writer has 6 rows (10 s maximum), then 1.5 s more. Then it starts a us-east-1 region outage. When the step ends, the app ends the outage and starts a switchover to e1-1.

3.21The DLQ processor copies messages to S3 (tour step 21)

A DLQ only holds messages. The Artemis DLQ of each region has a processor that moves its messages to the S3 bucket of the region. The processor writes the object first. It acks the message only after S3 sends 200.

DLQ lane of one region The Artemis DLQ sends messages to the DLQ processor. The processor writes objects to the S3 bucket, then acks the message. Artemis DLQmessage waits DLQprocessor S3 bucketdlq-archive PutObject,200, then ack
Figure 21-A. The DLQ lane of one region. The ack comes after the 200 from S3.
What occursResult
The region has an outage before the PUT gets to S3S3 has no object. The message stays in the DLQ.
S3 stored the object, but the ack is lostThe message stays in the DLQ. The next try overwrites the same key. The S3 overwrites counter increases.
The broker that holds a DLQ message is downThe message stays on that broker until the broker restarts. The other messages continue.
You set DLQ processor on to offA copy that is in progress completes. Then new messages stay in the DLQ.
CAUTION

The SQS DLQs have no processor. In real SQS, their messages do not stay for all time. On a standard queue, expiry starts at the original enqueue time, not at the move to the DLQ. Set the DLQ retention to more than the source retention. Simulation The app does not expire messages.

Simulation in this step: the processor starts off, and it comes on after 14 s. Poison: API always rejects (422) is 30%, and the publish rate is 2 events/s.

3.22Two UIs read through a Query API (tour step 22)

The steps before this one are about ingestion, so the tour hides the read path in them. This step shows it. Center and Cube only read. They are two separate deployments in each region.

  1. A UI sends a query to the Query API of its region.
  2. The Query API sends the query through the ALB to a pod of the existing API.
  3. The API pod reads the reader of its region.
  4. The answer goes back on the same path.
Read path of one region Center and Cube send queries to the Query API. The Query API sends them through the ALB to an API pod. The API pod reads the reader of its region. The answers go back on the same path. Centerown deployment Cubeown deployment Query APIHPA, 1 to 5 pods ALB APIshared pods Readersame region Top arrows: the query. Light blue: SQL. Green: the answer.
Figure 22-A. The read path of one region. It only reads. The API is the same API that the consumers call.

The Query API has its own HPA, from 1 to 5 pods by default. Reads and ingestion use the same pods of the existing API, and its HPA counts both. A Cube query costs the reader more than a Center query, so examine the ACUs of the reader. Each UI shows the rows that it can see. In us-east-2, this number can be less than the rows on the writer, by the replication lag.

Simulation in this step: publish rate 1 event/s, 1 query/s in each region. Center and Cube take turns.

NOTE

In the scenario results, heavy reads increase the API pods and the ACUs of the reader. They have almost no effect on the time until the last event finishes (scenarios 34 and 39, and 40 and 41 in 6.3). Timings in the app are illustrative, so load test a real system.

3.23Configuration checklist (tour step 23)

Use this checklist to examine the configuration of a real pipeline.

AreaCheck
SQS
  • Set the visibility timeout to more than the worst-case pod time.
  • Set maxReceiveCount.
  • Set the DLQ retention to more than the source retention.
Pods
  • Use a blocking, durable send. Delete the SQS message after the send.
  • Include the event id so that the API can remove duplicates.
Fan-out
  • One topic to the two regions causes two times the processing. Make sure that this is intended.
NLB
  • The NLB balances only connections, for producers and for consumers.
  • Keep Core pings (clientFailureCheckPeriod, 30 s default [28]) below the 350 s idle timeout.
  • Plan how clients rebalance after a broker restart. A broker can have no consumers.
Artemis
  • Set a dead-letter-address. If not, Artemis discards poison messages.
  • Decide if redistribution-delay must be 0 or more.
DLQ processor
  • Write to S3 before you ack.
  • Make the key from the message id, so that a retry overwrites.
  • Set redistribution-delay on the DLQ address, or connect to each broker.
  • Set an alarm on the DLQ depth.
Mirror
  • The mirror is async by default. sync=true gives more safety for more latency.
  • Keep per-region names (use1.*, use2.*) so that copies stay on standby.
  • Configure the address and queue of the other region on each broker.
  • Write a script for failover (which consumers attach to use1.queue) and for failback.
  • Remove duplicates in the API for the edge conditions.
Autoscaler
  • KEDA has an artemis-queue scaler. Each broker reports only its own message count.
ALB
  • Round robin does not look at the load on a pod.
  • Let pods drain before they stop, or a deployment causes 5xx responses and uses delivery attempts.
API
  • Ack only after a 2xx response.
  • Send 4xx for payloads that never pass. These go to the dead-letter address.
  • Send 5xx for errors that a retry can correct.
  • Set redelivery-delay so that retries do not overload a database during a failover.
Read path
  • Reads share the API with ingestion. Set the API maximum for both.
  • Set a timeout on each query.
  • Show how old the data is, because a reader can be behind the writer.
HPA
  • Scale on a metric that follows the load, for example in-flight requests. CPU alone is not sufficient.
  • The defaults are a 15 s sync period and a 300 s scale-down stabilization window.
  • Set maxReplicas to a value that the database can serve.
Aurora
  • Put a unique key on event_id. Use INSERT … ON CONFLICT DO NOTHING.
  • Do not remove duplicates with a read on a replica only.
  • Write through the global writer endpoint. Cross-region writes cost a round trip, or use write forwarding.
  • Set the ACU minimum and maximum for bursts.
  • Keep the failover reader in promotion tier 0 or 1.
Database failover
  • A global failover loses up to the replication lag. Monitor AuroraGlobalDBRPOLag. Think about rds.global_db_rpo.
  • Fail back with a switchover. A switchover loses nothing.

Simulation in this step: publish rate 1.2 events/s.

4Procedures

4.1Open the app pages

PageDeployed pathLocal path (npm run dev)
3D view/http://localhost:5173/
2D view/2dhttp://localhost:5173/2d.html
Manual/manualhttp://localhost:5173/manual.html
  1. Open the 3D page at /.
  2. To open the 2D page, click Open 2D version ↗ in the title card.
  3. To go back to the 3D page, click Open 3D version ↗ in the title card of the 2D page.
  4. To open this manual, click Manual in the title card.
NOTE

Each page runs its own simulation. A change on one page does not change the other page.

The deployed path /2d.html redirects to /2d. An unknown path shows a “Page not found” page with links to the two views.

4.2Select a view

  1. Find the view buttons at the top of the page.
  2. To see the live simulation, click Both regions (live).
  3. To see a close-up of the us-east-1 cluster, click Inside Artemis.
NOTE

A click on a view button stops the guided tour.

4.3Examine a part

  1. Make sure that Explore is selected.
  2. Move the pointer onto a part. A tooltip shows the name and the type of the part.
  3. Click the part. The info card shows the description, the data and the related parts.
  4. To go to a related part, click its button in the info card.
  5. To close the info card, click × or push Esc.
  6. In the 2D view, click one pod, one consumer or one broker to see its live data.

4.4Use the guided tour

The tour has 23 steps. Section 3 gives the text of each step. Steps 15 to 20 are about the API and Aurora. Step 21 is about the DLQ processor. Step 22 is about the read path.

NOTE

The tour hides the read path in each step except step 22, so that the other steps show only ingestion. The Show control changes during the tour. When the tour ends, it goes back to your setting.

CAUTION

Many tour steps change the simulation settings and the counters. Do not compare counters from the tour with scenario results.

  1. Click Guided tour. The tour starts at step 1.
  2. Read the text of the step. Look at the callouts in the scene.
  3. To go to the next step, click Next → or push the → key.
  4. To go to the previous step, click ← Back or push the ← key.
  5. To play the animation of the step again, click ↻ Replay.
  6. To go to a different step, click its mark in the progress bar.
  7. To stop the tour, click Explore or push Esc.

4.5Move the 3D camera

  1. To turn the model, drag with the left mouse button.
  2. To zoom, turn the scroll wheel.
  3. To pan, drag with the right mouse button.
  4. On a touch screen, drag with one finger to turn. Pinch with two fingers to zoom.
  5. To move the parts apart, open Display options. Then move the Exploded view slider.
NOTE

On a narrow screen, Display options starts closed. Tap its header to open it.

4.6Pan and zoom the 2D diagram

  1. To pan, drag the diagram.
  2. To zoom, turn the scroll wheel or pinch with two fingers.
  3. To fit the diagram to the window, double-click the diagram.

4.7Use URL parameters

You can give each state of the app as a link. Add the parameters after ?, and join them with &.

ParameterEffectPages
view=system, view=artemisOpens the view.3D, 2D
mode=guided&step=1 … step=23Opens the tour at the step. step alone also starts the tour.3D, 2D
pin=<part id>Opens with the info card of the part. Add view=artemis for a part of the “Inside Artemis” view.3D, 2D
panel=0Starts with the Simulate panel collapsed.3D, 2D
explode=0.6Opens the exploded view (0 to 1).3D only

Examples: /?view=artemis&pin=nlb, /2d?mode=guided&step=10, /?panel=0&explode=0.6, /2d?pin=aurora-e1-1, /?pin=e2-api.

4.8Use the Simulate panel

The Simulate panel is on the right side of the page. On a narrow screen, the panel starts collapsed. Tap its header to open it.

Control reference

SectionControlRange and defaultFunction
ShowComponentsIngestion + UIs (default), Ingestion onlyIngestion only hides Center, Cube and the Query API, and stops their queries. The tour sets this control for each step, and sets it back when the tour ends.
TrafficPublish rate0 to 8 events/s, step 0.1. Default 1.2.Sets the number of events for each second from the producer.
TrafficPublish 1ButtonPublishes one event.
TrafficBurst 25ButtonPublishes 25 events in approximately 1 s.
TrafficBurst 250, Burst 500, Burst 1000ButtonsPublishes 250, 500 or 1000 events in 1.5 s. SQS holds the spike. KEDA scales to its maximum (10 pods by default).
Capacity (per region)KEDA max pods (SQS)10, 20, 30, 40 or 50. Default 10.Sets the KEDA maxReplicaCount of the SQS consumer pods in each region. KEDA scales between 1 and this maximum. When you decrease it, each pod above the maximum stops after it completes its message.
Capacity (per region)Artemis consumer count10, 20, 30, 40 or 50. Default 10.Sets the number of Artemis consumers in each region. This number is fixed: nothing scales the consumers. New consumers connect through the NLB. When you decrease it, the extra consumers do not take new messages. Each one stops after it completes its message.
Capacity (per region)API max pods (HPA)10, 20, 30, 40 or 50. Default 10.Sets the HPA maxReplicas of the API Deployment in each region. The HPA scales between 1 and this maximum. When you decrease it, each pod above the maximum stops after its requests complete.
Capacity (per region)Query API max pods (HPA)5, 10, 20, 30, 40 or 50. Default 5.Sets the HPA maxReplicas of the Query API. The HPA scales between 1 and this value. If you lower it, pods above it finish their queries and then stop.
ClockSpeed0.25× to 3×, step 0.25. Default 1×.Sets the speed of the simulation clock.
ClockPause / ResumeButton, or the Space keyStops or starts the simulation clock. A pause also stops the animations of the guided tour.
ClockResetButtonClears the simulation, sets all controls to their defaults and clears the event log.
TopologyFan-out1 topic → both regions, Topic per region (default)Sets if each event goes to one region or to the two regions.
TopologyArtemis consumersPer-region queues (use1 / use2), copies standby (default), Same queue name, both regions consume, Same queue name, us-east-1 onlySets the queue names and the regions that consume. The copies stay on standby, or the other region consumes them, or only us-east-1 consumes.
TopologyMirrored queues on (copy to the other region)Switch. Default on.Off: new messages have no copy in the other region. A message that already has a copy keeps it until its ack arrives. The tour sets this switch to on, and sets it back when the tour ends.
FailuresSend 1 poison → SQS DLQButtonPublishes one event that a pod cannot convert to Avro.
FailuresSend 1 poison → Artemis DLQButtonPublishes one event that the API always rejects with 422.
FailuresPoison: Avro can’t be built0 to 50% of events, step 5%. Default 0%.These events fail on each receive. They go to the SQS DLQ after maxReceiveCount (3).
FailuresPoison: API always rejects (422)0 to 50% of events, step 5%. Default 0%.The API sends 422 for these events on each delivery. They go to the Artemis DLQ after max-delivery-attempts (3).
FailuresTransient failures0 to 50% of attempts, step 5%. Default 0%.Each attempt fails at random with this probability: at the Avro conversion in a pod, and as an API 500. The retry usually succeeds.
FailuresArtemis redelivery-delay0 to 5 s, step 0.5. Default 0 s.Sets the time before the broker delivers a rolled-back message again. A higher value lets the retries continue for longer than a database failover.
FailuresConsumer work time (before the API call)0.1 to 5 s for each message, step 0.1. Default 0.5 s.Sets the time that a consumer works before it calls the API. A long time makes a backlog in Artemis.
FailuresMirror lag0.2 to 4 s, step 0.1. Default 0.7 s.Sets the delay before a send or an ack gets to the other region.
FailuresRedistribution on (delay 0)Switch. Default off.On: redistribution-delay 0. Off: −1. On moves stored messages off a broker that has no consumers.
DLQ processingDLQ processor on (copy messages to S3)Switch. Default on.On: the processor moves Artemis DLQ messages to S3. Off: messages stay in the Artemis DLQ. Set it to on again and the DLQ drains. The SQS DLQ has no processor.
Read path (per region)Center queries0 to 5 /s, step 0.1. Default 0.5 /s.Sets the query rate of each Center deployment.
Read path (per region)Cube queries (analytics)0 to 3 /s, step 0.1. Default 0.5 /s.Sets the query rate of each Cube deployment. Each query costs the reader more than a Center query.
API (HPA)API time per request0.05 to 2 s, step 0.05. Default 0.25 s.Sets the work time in an API pod for each request. A higher value keeps more requests in flight, and the HPA adds pods (1 to the API max pods (HPA) value in each region).
AuroraReplication lag to us-east-20.1 to 5 s, step 0.1. Default 0.8 s.Sets the Global Database lag from the writer to the other region. A higher value gives more dedupe at write and more rows lost at a global failover.
AuroraServerless v2 max capacity1 to 32 ACU, step 0.5. Default 8 ACU. (Minimum: 0.5 ACU, fixed.)Sets the maximum ACUs of each instance. At the maximum, statements wait and become slower.
AuroraRead before write (dedupe on the local reader)Switch. Default off.Off: the API only writes, to the writer. On: the API first reads the reader of its region. If the row is there, the API does not write (dedupe at read).
AuroraPromote us-east-2 if us-east-1 is lost (global failover)Switch. Default on.On: a us-east-1 outage promotes e2-1 to writer after 5 s. Off: each write gets 503 until us-east-1 is back.
AuroraKill writer (e1-1) / Restart e1-…ButtonStops the current writer. Aurora promotes the other instance in the same cluster after 3 s (in-region failover). While an instance is down, the button shows Restart and its id. Click it to start that instance again as a reader.
AuroraSwitchover to e1-1Button. Not available while e1-1 is the writer.Starts a planned switchover that moves the writer back to e1-1 after 3 s, with no data loss. During a promotion, the switchover waits until the promotion completes.
Load balancingPods open a new connection per messageSwitch. Default off.On: each send gets its own NLB hash. Arrivals spread across the brokers, but each message needs a new connect and handshake.
Chaos · us-east-1Kill B1, Kill B2, Kill B3ButtonsStops the broker in us-east-1. The button then shows Restart B1 (B2, B3). Click it again to start the broker.
Chaos · us-east-1Reconnect clientsButtonReconnects all pods and consumers in us-east-1. The NLB hashes them again across the healthy brokers.
Chaos · us-east-1Region outage (us-east-1)Switch. Default off.Stops all of us-east-1, or starts it again. The outage also stops SNS and SQS in us-east-1, the us-east-1 API, e1-1 and e1-2.

Change the traffic

  1. Set the Publish rate slider to the rate that you want.
  2. To add one event, click Publish 1.
  3. To add a burst of events, click Burst 25.
  4. Look at the counters pods us-east-1 and pods us-east-2. KEDA adds pods when the queue grows.

Change the capacity

  1. Under Capacity (per region), find the four controls KEDA max pods (SQS), Artemis consumer count, API max pods (HPA) and Query API max pods (HPA).
  2. For each control, click 10, 20, 30, 40 or 50. Query API max pods (HPA) also has 5, its default. The value applies to each region.
  3. To make a load, click Burst 1000.
  4. Look at the pod grid, the consumer grid, the API grid and the Query API grid. Each grid shows more, smaller cells for a higher value.
  5. Look at the counters pods us-east-1, API pods us-east-1 and in SQS.
  6. To go back to the default capacity, click Reset.
NOTE

A decrease does not stop work. A pod, consumer or API pod above the new value completes its message or request first. Then it stops.

Change the topology

  1. Under Fan-out, click 1 topic → both regions or Topic per region.
  2. Under Artemis consumers, click one of the three options.
  3. Look at the counters deliveries / event and duplicates.

Cause failures

  1. To follow one poison message, click Send 1 poison → SQS DLQ or Send 1 poison → Artemis DLQ.
  2. To keep messages in the Artemis DLQ, set DLQ processor on to off. To drain them to S3, set it to on.
  3. Look at the event log. It shows each failed attempt.
  4. To make a percentage of events poison, set one of the two Poison sliders.
  5. To add random failures, set the Transient failures slider.
  6. To make a backlog in Artemis, set Consumer work time (before the API call) to a high value, for example 4 s.
  7. To change the delay of the mirror, set the Mirror lag slider.
  8. To move stranded messages to brokers with consumers, set Redistribution on.
  9. To delay each redelivery, set the Artemis redelivery-delay slider.

Show or hide the read path

  1. Open the Simulate panel. Show is the first section.
  2. To see only the ingestion pipeline, click Ingestion only.
  3. To see the UIs and the Query API, click Ingestion + UIs. This is the default.
  4. To change the load, set Center queries and Cube queries (analytics).
  5. Examine the counters queries sent, Center answered, Cube answered, queries failed and queries skipped.

The counters API 2xx, API 4xx and API 5xx count consumer calls only. At the default rates, each region sends 1 query each second, and Center and Cube take turns. If you set different rates, they do not take turns.

Scale the API

  1. Set API time per request to a high value, for example 0.8 s.
  2. Click Burst 25.
  3. Look at the counters API pods us-east-1, API pods us-east-2 and API queued.
  4. Look at the API pods. The ALB uses round robin, so each ready pod gets the same number of requests, also when it is busy.
  5. Look at the event log. It shows each HPA decision, for example “HPA us-east-1: 6 in flight ÷ 1 → 1 → 6 pods”.
  6. Wait approximately 15 s after the burst. The HPA removes the pods that it does not need.
NOTE

The counters API pods show only ready pods. A new pod is ready 2 s after the HPA adds it. In the 2D view, a pod that starts shows hatched.

Change the Aurora settings

  1. To increase the replica lag, set Replication lag to us-east-2 to a high value, for example 3 s.
  2. To make the API read before it writes, set Read before write (dedupe on the local reader) to on.
  3. To see duplicates removed, set Artemis consumers to Same queue name, both regions consume.
  4. Look at the counters dedupe at read, dedupe at write and rows stored.
  5. To limit the capacity, set Serverless v2 max capacity to a low value, for example 1 ACU.
  6. Set Publish rate to 8 events/s.
  7. Look at the counter ACU e1-1 / e1-2 / e2-1. The writer stops at the maximum, and the API pods increase.

Stop the writer and fail back

CAUTION

An instance that you stop stays down until you restart it. While the writer is down or a promotion is in progress, each write gets 503. Messages can then go to the Artemis DLQ, and the counters change.

  1. Click Kill writer (e1-1). The log shows the in-region failover to e1-2.
  2. Look at the counter writer. It shows promoting… for 3 s, then us-east-1 (e1-2).
  3. To start e1-1 again, click Restart e1-1. It comes back as a reader.
  4. To move the writer back to e1-1, click Switchover to e1-1.
  5. Look at the counter rows lost (RPO). It stays at 0: an in-region failover and a switchover lose nothing.
NOTE

To prevent dead-lettered messages during the failover, set Artemis redelivery-delay to 3 s or more before step 1. Compare scenarios 21 and 22 in 6.3.

Compare NLB balancing with cluster balancing

  1. Click Reset.
  2. Wait approximately 30 s.
  3. Write down the counters us-east-1 arrivals via NLB B1/B2/B3 and stored by cluster B1/B2/B3.
  4. Click Reset again.
  5. Set Pods open a new connection per message to on.
  6. Wait approximately 30 s.
  7. Compare the two counters with the values from step 3.

Result: with long-lived connections, the arrivals are not equal. With a new connection for each message, the arrivals are almost equal. The stored messages are equal in the two cases.

Stop a broker or a region

CAUTION

A broker that you stop stays down until you start it again. Its stored messages stay on it and the counters show them.

  1. Click Kill B1. The button changes to Restart B1.
  2. Look at the connection lines. They move to the other brokers.
  3. To start the broker again, click Restart B1.
  4. To send connections to the restarted broker again, click Reconnect clients.
WARNING

A region outage also stops the writer e1-1. With Promote us-east-2 if us-east-1 is lost (global failover) on, the global failover loses the rows that did not replicate to e2-1. In a real system, these rows are lost although their events were possibly acked. The failover can also cause duplicate processing.

  1. To stop all of us-east-1, set Region outage (us-east-1) to on.
  2. Look at the counters writer, API 5xx and rows lost (RPO). After 5 s, the writer is us-east-2 (e2-1).
  3. To start us-east-1 again, set Region outage (us-east-1) to off. e1-1 and e1-2 come back as readers.
  4. To move the writer back to us-east-1, click Switchover to e1-1. The app does not do this automatically.

Control the clock

  1. Set the Speed slider to make the simulation faster or slower.
  2. To stop the clock, click Pause. The button changes to Resume.
  3. To start the clock again, click Resume.
  4. To start again from zero with the default settings, click Reset.

Live counters

CounterMeaning
publishedEvents that the producer published.
unique processedEvents that consumers processed one or more times (a 2xx response, then an ack).
deliveries / eventSuccessful processings divided by unique processed events.
duplicatesProcessings of an event after its first processing.
in SQSMessages in the SQS queues of the two regions.
SQS DLQMessages that SQS moved to a DLQ.
in ArtemisMessages stored on all brokers, mirror copies included.
moved to Artemis DLQMessages that a broker moved to the dead-letter address. This total does not decrease when the processor removes a message.
waiting in Artemis DLQMessages that are in an Artemis DLQ now.
objects in S3Objects in the S3 buckets of the two regions.
S3 overwritesPUTs to a key that had an object. Each one is a retry after a lost ack.
mirror copies sentMessages that brokers copied to the other region. It stops when you set Mirrored queues on to off.
mirror backlogMirror events that wait because their target is down.
us-east-1 arrivals via NLB B1/B2/B3Messages that pods sent to each us-east-1 broker.
stored by cluster B1/B2/B3Messages that the us-east-1 cluster put on each broker, mirror copies included.
pods us-east-1, pods us-east-2Active pods in each region.
publish failedPublishes that failed because the region of the topic was down.
API pods us-east-1, API pods us-east-2Ready API pods in each region. Pods that start are not included.
API queuedRequests that wait: in a pod that is full, or at the ALB when no pod is ready. Queries of the read path are included.
API 2xx, API 4xx, API 5xxAPI responses to consumer calls: stored or already stored; 422 (poison); 500 (transient) or 503 (database not available).
rows storedRows on the writer. With no row loss, it is equal to unique processed.
Query API pods us-east-1, Query API pods us-east-2Ready Query API pods in each region.
queries sentQueries that Center and Cube sent, in the two regions.
Center answered, Cube answeredQueries of each UI that got a 2xx answer.
queries failedQueries that got a 5xx answer, or that the region lost in an outage.
queries skippedRefreshes that a UI did not send, because it had 20 open queries.
dedupe at readRequests that found their row on the local reader and did not write (only with Read before write on).
writes/s on the writerWrite statements that the current writer finished for each second, averaged over 5 simulated seconds. ON CONFLICT DO NOTHING statements count. The writer also shows this value on the diagram. After a promotion, the new writer starts from 0.
dedupe at writeWrites that found their row already on the writer: ON CONFLICT DO NOTHING.
cross-region writesRows that an API stored on a writer in the other region.
rows lost (RPO)Committed rows that a global failover lost, because they did not replicate to e2-1.
ACU e1-1 / e1-2 / e2-1Current capacity of each instance, for example 2.0 / 0.5 / 0.5.
writerThe region and the id of the current writer, for example us-east-1 (e1-1). During a promotion: promoting….

Event log

The event log shows the last 50 entries. Each entry starts with the simulation time. Yellow entries are warnings, light red entries are failures and green entries are recoveries.

Entries for the API and Aurora:

  • HPA decisions, for example “HPA us-east-2: 4 in flight ÷ 1 → 1 → 4 pods (new pods ready in 2s)”.
  • ACU changes, only when the integer part changes, for example “Aurora e1-1 (writer): 0.5 → 1.0 ACU (utilisation 120%)”.
  • 503 bursts, one entry for each second, for example “API us-east-2: 6× 503 in the last second (writer promotion in progress); consumers roll back”.
  • Read path: “Read path ON” and “Read path OFF”, “HPA Query API us-east-1: …” for its pods, and failed queries, one entry for each second, for example “Read path us-east-2: 2 queries failed in the last second (no Aurora instance up in us-east-2 to read from)”.
  • Dedupe, failover, promotion and lost rows, for example “Aurora: 3 committed rows had not replicated to e2-1 → lost (RPO)”.

4.9Run a scenario by hand

  1. Open the 3D page or the 2D page in Explore mode.
  2. Click Reset.
  3. Set the controls that the table gives for the scenario.
  4. Wait approximately 60 s.
  5. Read the counters and the event log. Compare them with the column “What to look for”.
NOTE

The script npm run scenarios uses a publish rate of 2 events/s and a seeded random generator. Your numbers by hand are not the same as the script numbers. Scenarios 15 to 24 use the API and Aurora. Scenarios 25 and 26 use the capacity controls. Some of them use console commands (4.11).

#ScenarioSettings after ResetWhat to look for
1BaselineNo change: Topic per region, Per-region queues.1.00 deliveries / event. Cyan copies appear in the other region, and acks delete them.
2One topic → both regionsFan-out: 1 topic → both regions.2.00 deliveries / event. Each region processes its own copy of each event.
3Same queue name, both consumeArtemis consumers: Same queue name, both regions consume.Approximately 2 deliveries / event. The other region takes the mirror copy before the ack arrives.
4Same queue name, us-east-1 onlyArtemis consumers: Same queue name, us-east-1 only.1.00 deliveries / event. The us-east-2 consumers are idle.
5One topic, same name, both consumeThe settings of scenarios 2 and 3 together.Up to approximately 4 deliveries / event.
6Poison at AvroClick Send 1 poison → SQS DLQ, or set Poison: Avro can’t be built to 10%.A red-orange cube is received 3 times. Then it moves to the SQS DLQ.
7Poison at the APIClick Send 1 poison → Artemis DLQ, or set Poison: API always rejects (422) to 10%.The API sends 422 3 times. Then the message moves to the Artemis DLQ.
8Transient failuresTransient failures: 20%.Most retries succeed. The DLQs stay almost empty.
9Broker fails under load
  1. Set Consumer work time (before the API call) to 4 s.
  2. Set Publish rate to 3.
  3. Wait approximately 20 s.
  4. Click Kill B1.
  5. After 10 s, click Restart B1.
Messages on B1 stay stranded. B1 has no consumers until you click Reconnect clients.
10Broker failure with redistributionDo scenario 9 with Redistribution on.Stranded messages move to brokers that have consumers.
11Region failover
  1. Set Region outage (us-east-1) to on.
  2. Wait approximately 10 s.
  3. Set it to off.
The producer moves to us-east-2. The us-east-2 consumers take over use1.queue. One duplicate or a few duplicates occur at the switch. The Aurora writer moves to e2-1.
12Failover with redistributionDo scenario 11 with Redistribution on.The seeded script gives the same result as scenario 11.
13Outage with one topicSet Fan-out to 1 topic → both regions. Then do scenario 11.Publishes fail while us-east-1 is down. The topic is a single point of failure.
14NLB compared with cluster balancingDo the procedure in Compare NLB balancing with cluster balancing.Arrivals are not equal with long-lived connections. Stored messages are equal in the two cases.
15High rate: the HPA and the writer ACUsPublish rate: 8.The API pods increase in the two regions. The writer ACUs increase to the maximum. The readers stay small.
16BurstsClick Burst 25 three times, wait 20 s, then do it again.The API pods increase at once and decrease approximately 10 s after each burst.
17Dedupe at read and at write
  1. Set Artemis consumers to Same queue name, both regions consume.
  2. Set Replication lag to us-east-2 to 2 s.
  3. Set Read before write to on.
dedupe at read and dedupe at write both increase. rows stored stays equal to unique processed.
18Region outage with global failoverPublish rate 6. Then do scenario 11.The us-east-2 API gets 503 for 5 s. Then e2-1 is the writer. rows lost (RPO) increases.
19Region outage without global failoverSet Promote us-east-2 if us-east-1 is lost to off. Then do scenario 18.Each write gets 503 during the outage. More messages go to the Artemis DLQ. No rows are lost.
20In-region failover and switchoverDo the procedure in Stop the writer and fail back.Writes get 503 for 3 s. e1-2 becomes the writer. After the switchover, e1-1 is the writer again. No rows are lost.
21Writer down with no replica
  1. Type __sim.killDb('e1-2').
  2. Type __sim.killDb('e1-1').
  3. After 6 s, type __sim.restartDb('e1-1'). It becomes the writer again.
  4. Type __sim.restartDb('e1-2').
Each write gets 503 while the writer is down. Some messages use all 3 attempts and go to the Artemis DLQ.
22Same, with a redelivery delaySet Artemis redelivery-delay to 3 s. Then do scenario 21.The retries continue for longer than the outage. No message goes to the Artemis DLQ.
23Reader e2-1 downType __sim.killDb('e2-1'). After 5 s, type __sim.restartDb('e2-1').No effect on the requests, because the API only writes by default.
24Switchover during a failoverClick Kill writer, then Restart e1-1, then Switchover to e1-1, each 1 s after the other.The log shows “Switchover queued”. The switchover starts when the promotion of e1-2 completes. Writes get 503 for approximately 6 s: two promotions, one after the other.
25Spike at the default capacitySet Publish rate to 0. Click Burst 1000.SQS holds the spike. 10 pods, 10 consumers and up to 10 API pods in each region process it. The drain takes some minutes.
26Spike at the full capacitySet the three Capacity (per region) controls to 50. Set Publish rate to 0. Click Burst 1000.The pods and the API pods increase to approximately 50 in each region. The drain is approximately 3 times faster than in scenario 25. The writer ACUs increase to the maximum.
27Processor onSet the two poison sliders to 10%.Artemis DLQ cubes move to S3. waiting in Artemis DLQ goes back to 0. SQS DLQ cubes stay.
28Processor offSet the two poison sliders to 10%. Set DLQ processor on to off.waiting in Artemis DLQ increases. objects in S3 stays 0.
29Processor on lateDo the scenario before this one. After 30 s, set DLQ processor on to on.The Artemis DLQ drains to S3.
30Outage while the processor runsSet the two poison sliders to 20%. Set Region outage to on for 10 s.Processing stops in us-east-1 during the outage. After it, the DLQ drains.
31Broker failure while the processor runsSet Poison: API always rejects (422) to 20%. Click Kill B1. After 10 s, click Restart B1.Messages on B1 wait. After the restart, they go to S3.
32Read path, default ratesUse the default settings.Queries go from each UI to the reader of its region. All queries get an answer.
33Heavy readsSet Center queries to 5 /s and Cube queries to 3 /s.The Query API goes to 5 pods. The existing API adds pods. The ACUs of the readers increase. Ingestion continues.
34Heavy reads, slow APIDo scenario 33. Set API time per request to 2 s.queries skipped increases. The queries take more time to drain. The last event finishes approximately at the same time as in scenario 39.
35Outage with readsSet Region outage to on for 10 s.The UIs in us-east-1 show region down. The UIs in us-east-2 continue.
36No reader in us-east-2In the console, type __sim.killDb('e2-1'). After 5 s, type __sim.restartDb('e2-1').The UIs in us-east-2 show query failed. Ingestion does not stop.
37No reader in us-east-1In the console, type __sim.killDb('e1-2'). After 5 s, type __sim.restartDb('e1-2').The queries go to the writer. Only a query that is on the reader at that moment fails.
38Hide the read pathAfter 30 s, click Ingestion only.The queries stop. Ingestion does not change.
39Slow API, no readsClick Ingestion only. Set API time per request to 2 s.Use this result to compare with scenario 34.
40High rate, slow API, heavy readsSet Publish rate to 6 events/s and API time per request to 2 s. Set the query rates of scenario 33.The backlog stays in Artemis. Compare with scenario 41.
41High rate, slow API, no readsDo scenario 40. Click Ingestion only.Use this result to compare with scenario 40.
42Heavy reads, larger Query APIDo scenario 33. Set Query API max pods (HPA) to 20.Fewer queries are skipped than in scenario 33. The queries drain sooner.
43Lower the maximumDo scenario 42. After 30 s, set Query API max pods (HPA) to 5.Pods above 5 finish their queries, then stop.
44Mirroring offSet Mirrored queues on to off.The cyan lines go away. mirror copies sent stops. Processing does not change.
45, 46Short outage, mirroring on and offSet Region outage to on for 10 s, with the switch on, then with it off.With it off, the log shows “No failover”. The messages of us-east-1 wait on its brokers, then finish.
47Shared queue name, no copiesSet Artemis consumers to Same queue name, both regions consume. Set the switch to off.deliveries / event is 1.00. Compare scenario 3.
48Turn it off while it runsAfter 30 s, set the switch to off.Existing copies go away when their acks arrive.
49Turn it on, then an outageStart with the switch off. After 30 s, set it to on. After 10 s, set Region outage to on for 10 s.Only messages from after the switch have a copy.
50, 51Region lost for goodSet Consumer work time to 4 s and Publish rate to 3. After 20 s, set Region outage to on. Do not set it to off. Do this with the switch on, then off.With it off, fewer events finish: the messages stay on the lost brokers.

4.10Run all scenarios with npm

  1. Open a terminal in the project folder event-pipeline-v2-3d.
  2. If this is the first run, type npm install.
  3. Type npm run scenarios.
  4. To change the traffic time of each scenario, type npm run scenarios -- --seconds 120.
  5. To run the simulation in Node.js without a browser, type npm run scenarios -- --node. The numbers are the same.
  6. Compare the table with 6.3.

The script opens the 2D page in a headless browser. It runs each scenario for 60 simulated seconds of traffic (default). Then it runs until nothing moves, for a maximum of 10 simulated minutes. The random generator has a fixed seed, so the numbers are the same each time.

The base settings of the script are: publish rate 2 events/s, Topic per region, Per-region queues, mirror lag 0.7 s, consumer work time 0.5 s, API time 0.25 s, replication lag 0.8 s, maximum 8 ACU, global failover on, Read before write off, redelivery delay 0, and capacity 10 / 10 / 10 in each region (KEDA max pods, Artemis consumers, API max pods). The column drain time (s) is the simulated time from the first publish until nothing moves or waits. The column stuck in Artemis counts messages that stay in Artemis after all other work stops. A stranded event counts two times: the original message and its mirror copy.

The script also examines these conditions after each scenario. The column check shows ok if they are all true:

  • Each row belongs to a processed event. (Exception: a row of an event that went to the dead-letter address after its 200 was lost.)
  • Each processed event has a row, unless a global failover lost it.
  • The rows are not more than the unique events.
  • At the end, nothing moves or waits.
  • Each query that a UI sent got an answer or failed, and no query stays open.
  • If a region stays down at the end (scenarios 50 and 51), the check does not need that region to be idle, because its SQS queue keeps its messages.

Scenarios 1 to 31, 39, 41 and 44 to 51 run with the read path off. The other scenarios turn it on.

The column acked, no row counts processed events that have no row in Aurora. Only a global failover (RPO) causes this.

4.11Control the simulation from the console

  1. Open the developer tools of the browser.
  2. Select the Console tab.
  3. To change settings, type __sim.set({ rate: 5, consume: 'sharedBoth' }).
  4. To stop broker 2 in us-east-1, type __sim.kill('e1', 1).
  5. To read the counters, type __sim.counts().
  6. To make the HPA add API pods, type __sim.set({ apiTime: 0.8 }).
  7. To turn on the read before write, type __sim.set({ readCheck: true }).
  8. To see more dedupe at write, type __sim.set({ replLag: 3, consume: 'sharedBoth' }).
  9. To stop the writer, type __sim.killDb('e1-1'). To start it again, type __sim.restartDb('e1-1').
  10. To move the writer back to e1-1, type __sim.switchover().
  11. To turn off the mirrored queues, type __sim.set({ mirroring: false }).
  12. To hide the read path, type __sim.set({ readPath: false }). To change its load, type __sim.set({ centerRate: 1, cubeRate: 1, qapiMax: 10 }).
  13. To read the API and Aurora values, type __sim.counts().acu, __sim.counts().rowsLost or __sim.db.writer.
NOTE

The broker index starts at 0. Thus __sim.kill('e1', 1) stops broker 2. Other functions are publish(), burst(n), restart(region, k), reconnect(region), outage(region, true|false) and reset().

The instance ids for killDb and restartDb are e1-1, e1-2 and e2-1. restartDb does nothing while the region of the instance is down. The settings for set() are the keys in 6.2, for example apiTime, replLag, acuMax, readCheck, dbFailover and redeliveryDelay.

__sim.counts() also gives these API and Aurora values: apiPodsE1, apiPodsE2, apiQueued, apiOk, api4xx, api5xx, rows, writesPerSec, dedupRead, dedupWrite, crossWrites, rowsLost, acu (text, for example '2.0 / 0.5 / 0.5') and writer (text, for example 'us-east-1 (e1-1)'). For the read path, it gives queries, queriesOk, queriesFailed, queriesSkipped, centerOk, cubeOk, qapiPodsE1 and qapiPodsE2.

4.12Run the screenshot checks

The checks start their own Vite server on a free port. They open the app in headless Chromium.

CAUTION

npm run shoot deletes the folder shots/ and writes it again. npm run shoot:2d does the same with shots-2d/. Copy old screenshots before you start a check.

3D check

  1. Type npm run shoot.
  2. To check only some tour steps, type npm run shoot -- --steps 3,4.
  3. To skip the phone screenshots, add --no-mobile. To skip the view screenshots, add --no-views.
  4. Read the last line. “OK: no errors or broken references” tells you that the check passed.
  5. If the check fails, examine shots/report.json. It lists console errors, broken tour or part references and blank frames.

2D check

  1. Type npm run shoot:2d.
  2. To check only some tour steps, type npm run shoot:2d -- --steps 3,4.
  3. Read the last line. “OK: no errors” tells you that the check passed.
  4. Examine the screenshots in shots-2d/.

4.13Build and preview

  1. Type npm run build. Vite writes dist/ with index.html, 2d.html, manual.html and assets/.
  2. To serve the build with the Cloudflare runtime, type npm run preview:cf. This command builds the site and starts wrangler dev, with the same routing as production.
  3. Open the URL that wrangler shows.
  4. Make sure that /, /2d and /manual open.
NOTE

For work on the code, type npm run dev. The dev server reloads the pages when you save a file.

4.14Deploy to Cloudflare

The app deploys as a Cloudflare Worker with static assets. The code in worker/index.js handles only /api/*, for the notes. Each reader has one Durable Object, with their email from Cloudflare Access as its key. The file wrangler.jsonc holds the configuration.

CAUTION

npm run deploy replaces the live Worker. Run the checks in 4.12 before you deploy.

Deploy from your computer

  1. Type npx wrangler login. Do this one time only.
  2. Type npm run deploy. The command builds the site and starts wrangler deploy.
  3. Open https://event-pipeline-v2.<your-subdomain>.workers.dev/.
  4. Make sure that /, /2d and /manual open.

Make the notes sync

The API answers 401 if Cloudflare Access does not protect it. Notes then stay in the browser of the reader. You must do this procedure in the Cloudflare dashboard, because wrangler deploy does not change Access.

  1. In Cloudflare Zero Trust, add a self-hosted Access application for event-pipeline-v2.tudum.workers.dev.
  2. Set the path of the application to api/. The app and the manual stay public.
  3. Add a policy that lets in your account. Do not use the email domain proton.me in a policy, because other people have that domain.
  4. Type curl -I https://event-pipeline-v2.tudum.workers.dev/api/annotations. A 302 response to cloudflareaccess.com tells you that Access protects the API.
  5. Open the manual. Click ✎ Notes, then Sign in to sync. After the login, the page comes back to the manual.
NOTE

To pin the API to this one Access application, add its AUD tag to vars in wrangler.jsonc as ACCESS_AUD. On npm run preview:cf, the API uses one local reader and needs no Access.

Optional: to use a custom domain, remove the comment marks from routes in wrangler.jsonc. The domain must be on your Cloudflare account.

To change the name of the Worker and its URL, change name in wrangler.jsonc.

5Fault isolation

Use this table when the app or a real pipeline shows a problem. Find the symptom, then do the action. Rows with “real system” apply only to a real pipeline.

SymptomPossible causeAction
deliveries / event is 2.00. The duplicates counter increases for each event.Fan-out is 1 topic → both regions.Set Fan-out to Topic per region. In a real system, make sure that two times the processing is intended.
deliveries / event is approximately 2 with a topic per region. Cyan copies are consumed.Artemis consumers is Same queue name, both regions consume.Use per-region names (use1.*, use2.*). Do not have active consumers on the two sides.
A red-orange cube is received 3 times. Then the SQS DLQ counter increases.Poison message: the pod cannot convert the payload to Avro.Examine the message in the SQS DLQ. Correct the payload or the converter.
Healthy messages are received two times (real system).The visibility timeout is shorter than the worst-case pod time.Set the visibility timeout to more than the Avro conversion time plus the Artemis send time.
The moved to Artemis DLQ counter increases. The log shows “API 422 (poison: payload rejected), consumer rolled back; redelivery”.The API always rejects the message (poison), or there are many transient failures (API 500).Examine the messages at the dead-letter address. Correct the API or the message.
waiting in Artemis DLQ increases, and objects in S3 does not change.DLQ processor on is off, or the region or the broker that holds the messages is down.Set DLQ processor on to on. Restart the broker or end the outage.
Poison messages disappear, and no DLQ gets them (real system).No dead-letter-address in address-settings. Artemis discards the messages.Set a dead-letter-address.
The log shows transient failures. The DLQs stay almost empty.Transient failures. The retries succeed.No action is necessary.
The counter us-east-1 arrivals via NLB is not equal across brokers, for example 52 / 8 / 0.The NLB pins each long-lived connection to one broker.This is normal. Examine stored by cluster: ON_DEMAND makes it equal. To spread arrivals, use a new connection for each message. This adds a handshake for each message.
A broker has no consumers, for example 5 / 5 / 0.The NLB hash of the consumer connections.ON_DEMAND sends new arrivals to brokers with consumers. Set redistribution-delay to 0 or more for stored messages. Reconnect the clients to hash them again.
After a broker restart, in Artemis does not decrease. The broker has no consumers.The connections stay pinned to the other brokers. The stored messages are stranded.Click Reconnect clients, or set Redistribution on.
The cyan mirror lines are not visible. mirror copies sent does not increase.Mirrored queues on is off.Set it to on. Only new messages get a copy.
The log shows “No failover for use1.queue: mirroring is off”. in Artemis does not decrease during an outage.Mirroring is off, so the other region has no copy. The messages wait on the brokers of the region that is down.End the outage. They then finish. If the region does not come back, they are lost to the pipeline (scenarios 50 and 51). In a real system, keep mirroring on, or accept this risk.
The mirror backlog counter increases.The target broker or the target region is down.Start the target again. The log then shows “target back, … queued mirror events sent”.
The publish failed counter increases.One-topic mode and us-east-1 is down. The topic is a single point of failure.Use a topic per region. Start us-east-1 again.
Duplicates occur after a region failover.us-east-1 processed the message, but the ack did not get to us-east-2 before the outage.Remove duplicates in the API with the event id: a unique key and ON CONFLICT DO NOTHING.
The duplicates counter increases, but rows stored stays equal to unique processed.Normal. The API removes the duplicates: dedupe at write (and dedupe at read, if Read before write is on).No action is necessary. In a real system, make sure that each event has a unique event_id.
Many API 5xx after a us-east-1 outage starts. The log shows “503 in the last second (writer promotion in progress)” or “(writer e1-1 is down)”.The writer was in us-east-1. Writes get 503 until e2-1 is promoted (5 s in the app, minutes in a real system). If Promote us-east-2 if us-east-1 is lost is off, writes get 503 until us-east-1 is back.Wait for the promotion. Set Artemis redelivery-delay so that the retries continue for longer than the promotion. If the switch is off, end the outage.
The moved to Artemis DLQ counter increases during a writer failover. The log shows “failed 3× (last: API 503) → dead-letter address”.redelivery-delay is 0. Three fast 503 responses use all delivery attempts before the new writer is ready.Set Artemis redelivery-delay to 3 s or more (scenario 22). In a real system, set redelivery-delay and max-delivery-attempts to cover the failover time.
The rows lost (RPO) counter increases after a us-east-1 outage.Global failover. The old writer committed these rows, but they did not replicate to e2-1 before the outage.In the app, set a lower Replication lag to us-east-2. In a real system, monitor AuroraGlobalDBRPOLag and think about rds.global_db_rpo. Use a switchover, not a failover, for planned moves.
The writer stays us-east-2 (e2-1) after us-east-1 is back.The app does not fail back automatically after a global failover.Click Switchover to e1-1.
The log shows “Switchover: e1-1 is down; restart it first”.e1-1 is down, for example after Kill writer.Click Restart e1-1. Then click Switchover to e1-1.
Each write gets 503, and the counter writer shows us-east-1 (e1-1) DOWN. The log shows “DOWN with no replica in its cluster”.The writer stopped, and no other instance in its cluster is up. No failover can occur.Restart the writer (__sim.restartDb('e1-1')). Then restart the reader.
The us-east-2 API sends 503. The log shows “no Aurora instance up in us-east-2 to read from”.Read before write is on, and e2-1 is down.Restart e2-1 (__sim.restartDb('e2-1')), or set Read before write to off.
Center, Cube and the Query API are not visible.Show is Ingestion only, or a tour step hides them.Click Ingestion + UIs, or stop the tour.
Center and Cube show “query failed”. The log shows “no Aurora instance up in us-east-2 to read from”.No Aurora instance is up in that region.Restart the instance, for example __sim.restartDb('e2-1'). The writes of the consumers do not stop.
Center and Cube show “region down”. queries failed increases one time, then stops.The region has an outage. Its UIs stop, and its open queries fail. The app has no failover for the UIs.End the outage. The UIs in the other region continue.
queries skipped increases.A UI has 20 open queries, because the query rate is too high for the Query API or the API.Decrease the query rates, or increase Query API max pods (HPA).
The Query API status shows “queued” in yellow.All its ready pods are full, or new pods are starting.Wait for the HPA. If it stays at the maximum, increase Query API max pods (HPA).
The API pods or the ACUs of a reader increase, but the publish rate is low.The queries of the read path take API slots and reader capacity. A Cube query costs 4 times a Center query.This is normal. Decrease the query rates or click Ingestion only to compare.
Center in us-east-2 sees fewer rows than the writer.Replica lag. The rows did not replicate to e2-1 yet.This is normal. In a real system, show how old the data is.
The writer ACU stays at the maximum. API queued and the API pods increase. Requests are slow.The Serverless v2 max capacity is too low for the write load. All writes from the two regions go to the one writer.Increase Serverless v2 max capacity, or decrease the publish rate. In a real system, set the ACU maximum for bursts and limit maxReplicas of the HPA.
The API pods do not decrease after the load stops.Scale-down stabilization. The HPA uses the highest recommendation of the last 10 s (300 s by default in a real system). It does not remove a pod with requests in flight.This is normal. Wait.
dedupe at write increases although Read before write is on. The log shows “e2-1 was stale”.Replica lag. The reader did not have the row yet, so the read did not find it.This is normal: the unique key on the writer removes the duplicate. Do not remove duplicates with a read only.
A reader shows “lag: N rows” or hollow row cubes (2D view).Replica lag. The rows are on the writer, but did not replicate to this reader yet.This is normal. A higher Replication lag to us-east-2 increases the lag of e2-1.
The log shows “its row was already committed: an earlier 200 was lost”.A write committed, but its response was lost with the region. Then all retries failed.The event is stored. If you send the message again from the dead-letter address, the unique key removes the duplicate.
Each us-east-2 request takes longer than a us-east-1 request (real system).Each us-east-2 write crosses to the writer in us-east-1 and pays for a round trip.This is the cost of one writer. Think about write forwarding on the secondary cluster.
A message is not in us-east-2 after a us-east-1 failure (real system).The async mirror did not copy the message before the failure.Wait for the recovery of us-east-1. The message stays on its disk. Use sync=true if the added latency is permitted.
The number of pods changes slowly (real system).KEDA polls every 30 s and has a cooldown period of 300 s by default.This is normal. Change the polling interval or the cooldown period if necessary.
Idle client connections close (real system).The NLB TCP idle timeout is 350 s.Keep Core pings (clientFailureCheckPeriod, 30 s default [28]) below 350 s.
All us-east-2 consumers are idle.Artemis consumers is Same queue name, us-east-1 only. In per-region mode, they read only use2.queue.This is normal for these settings.
/manual or /2d shows “Page not found”.The page is not in the build.Make sure that vite.config.js lists the page in the build inputs. Build again.
The Simulate panel is not visible on a phone.On a narrow screen, or with panel=0, the panel starts collapsed.Tap the header of the panel.
The notes panel shows “Not signed in” or “Saved on this device”.You have no Access session, so the notes are only in this browser.Click Sign in to sync. Your local notes upload after the login.
Sign in to sync shows “Sign-in did not work”.Access does not protect /api/*, or it did not let your account in.Do the procedure in Make the notes sync.
Counters change, but you did not touch a control.A tour step changes the simulation.Stop the tour. Click Reset.
npm run shoot shows “NOT OK”.A console error, a broken reference or a blank frame.Examine shots/report.json. Correct the error and run the check again.

6Reference data

6.1Documented values

These values come from the AWS, KEDA, Kubernetes, PostgreSQL and Artemis documentation. The numbers in brackets refer to Section 8.

QuantityValueSource
SQS visibility timeout30 s default (range 0 to 43,200 s)[6]
SQS retention4 days default, 14 days maximum[6]
SQS DLQSame account, region and queue type as the source queue[6]
Message expiry in an SQS DLQStandard queue: from the original enqueue time. FIFO queue: from the move to the DLQ[6]
S3 consistencyStrong read-after-write, also for overwrites; updates to one key are atomic[24]
ALB routingRound robin by default; each request is routed independently[25]
ALB deregistration delay300 s default[25]
SQS redriveWhen ReceiveCount is more than maxReceiveCount[6]
SNS → SQS retries100,015 attempts over 23 days (fixed policy)[4]
SNS subscription DLQFor each subscription; same account and region as the subscription[4]
NLB target selection (TCP)Flow hash of protocol, source and destination IP and port, TCP sequence number. One connection goes to one target for its life.[8]
NLB TCP idle timeout350 s default (range 60 to 6000 s)[8]
NLB cross-zone load balancingOff by default[8]
KEDA aws-sqs-queue queueLength5 messages for each pod (default). Counts visible and in-flight messages.[9]
KEDA polling interval, cooldown period, maximum replicas30 s, 300 s, 100 replicas (defaults)[9]
KEDA Artemis scalerartemis-queue. Reads MessageCount through Jolokia. queueLength default 10.[9]
Artemis max-delivery-attempts10 (default)[14]
Artemis redistribution-delay−1 (default, never)[13]
Artemis message-load-balancingON_DEMAND (default)[13]
Artemis mirrorAsync by default; sync=true is optional[12]
Confluent wire format0x00 magic byte, then a 4-byte schema id[10] [11]
Artemis redelivery-delay0 (default)[14]
HPA sync period15 s default (--horizontal-pod-autoscaler-sync-period)[15]
HPA ruledesiredReplicas = ceil[currentReplicas × (currentMetricValue ÷ desiredMetricValue)][15]
HPA stabilization windowScale-up 0 s, scale-down 300 s (defaults)[15]
HPA tolerance10% (default)[15]
Serverless v2 ACUApproximately 2 GiB of memory, with matching CPU and network[16]
Serverless v2 scaling stepAs small as 0.5 ACU; larger steps at higher capacity[16]
Serverless v2 range0.5 to 128, 0.5 to 256 or 0 to 256 ACU, by engine and platform version; 0 ACU = auto-pause[16] [17]
Aurora readers in a clusterUp to 15[21] [23]
Reader lag in a clusterUsually much less than 100 ms[23]
In-region failoverTypically less than 60 s, often less than 30 s[21]
Global Database secondariesUp to 10 regions, read-only[18]
Global Database replication lagTypically less than 1 s[18]
Global failoverRPO typically seconds, RTO minutes; can lose write transactions that did not replicate[19]
Global switchoverRPO 0 (it waits for the secondary to synchronize)[19]
rds.global_db_rpo20 s to 2,147,483,647 s; not set (−1) by default[19]
Write forwarding (Aurora PostgreSQL)14.9+, 15.4+, 16+; off by default; apg_write_forward.consistency_mode default SESSION[20]
INSERT … ON CONFLICT DO NOTHINGDoes not insert the row if a conflict occurs. Needs a unique index or constraint (here on event_id) as the arbiter.[22]

6.2Simulation values

All values in this table are illustrative values of the app (src/content/sim.js). Simulation

ItemValueDocumented value
ALBRound robin over the ready pods. No latency or errors of its ownRound robin is the default
DLQ processor1 worker in each region, 1 message at a time, approximately 1.45 s for each messageModel assumption
Read pathOne Center, one Cube and one Query API Deployment in each region. Query API: HPA from 1 to 5 pods (maximum 5 to 50 in the panel), with the same rules as the existing API. A UI holds a maximum of 20 open queries. A Cube query takes 4× the time and costs 4× the ACUs of a Center queryModel assumption
Mirrored queues switch (mirroring)On. Off stops new copies. Acks still go out for messages that have a copyModel of a mirror broker connection that is off
Query rates (centerRate, cubeRate)0.5 queries/s each, in each region (ranges 0 to 5 and 0 to 3). Together, 1 query/s, and they take turnsModel assumption
Query API pods (QAPI_MIN, qapiMax)1 minimum, 5 maximum (5 to 50 in the panel). Same HPA poll, target, window, start time and requests for each pod as the APISet by you
Writes per secondAverage of the last 5 simulated seconds (WRITE_WINDOW)—
Visibility timeout3 s30 s default
maxReceiveCount3Set by you
max-delivery-attempts310 default
KEDA polling interval2 s30 s default
KEDA target5 messages for each pod5 default
Pods for each region1 minimum, 10 maximum, no cooldownMaximum 100, cooldown 300 s
Artemis consumers for each region10, fixedModel assumption
Brokers for each region3Model assumption
Default publish rate (rate)1.2 events/s (range 0 to 8)—
Default mirror lag (lag)0.7 s (range 0.2 to 4 s)—
Default consumer work time (procTime)0.5 s (range 0.1 to 5 s)—
Default redelivery delay (redeliveryDelay)0 s (range 0 to 5 s)0 default
SNS retry when the queue region is downEvery 2 s100,015 attempts over 23 days
SNS when its own region is downNo delivery. Accepted messages wait, then arrive after the outageAWS documents storage across data centers, but not this case
API pods for each region (HPA_MIN, HPA_MAX)1 minimum, 10 maximumSet by you
HPA poll (HPA_POLL)3 s15 s default
HPA target (HPA_TARGET)1 request (in flight + queued) for each podSet by you
HPA scale-down window (HPA_DOWN)10 s300 s default
Time until a new API pod is ready (POD_START)2 s—
Requests at the same time for each API pod (API_CONC)2—
Default API time (apiTime)0.25 s (range 0.05 to 2 s)—
Serverless v2 capacity (acuMin, acuMax)0.5 ACU minimum (fixed), 8 ACU maximum by default (range 1 to 32)0 or 0.5 to 256 ACU
ACU poll (ACU_POLL)1 s—
ACU demand for each statement (ACU_WRITE, ACU_READ, ACU_SCAN)Write 0.45 ACU-seconds, read 0.15 ACU-seconds, Cube query 0.6 ACU-seconds—
ACU thresholds (ACU_UP, ACU_DOWN, ACU_DOWN_POLLS)Scale up above 70%; scale down below 30% after 3 pollsReal scaling uses CPU, memory and network load
Statement time (DB_OP, DB_SLOW)0.12 s; up to 4 times longer above 100% utilization. A Cube query takes 4 times longer (SCAN_TIME)—
Parallel statements for each instanceACU × 2, rounded, minimum 1—
Reader lag in the primary cluster (LOCAL_LAG)0.15 sMuch less than 100 ms
Default replication lag to us-east-2 (replLag)0.8 s (range 0.1 to 5 s)Typically less than 1 s
In-region failover (DB_LOCAL_FAILOVER)3 s (switchover: also 3 s)Typically less than 60 s
Global failover (DB_PROMOTE)5 sMinutes
Global failover switch (dbFailover)On—
Read before write (readCheck)Off—
Travel times
  • Publish: 0.5 s
  • Fan-out: 0.7 s (same region), 1.2 s (cross-region)
  • Receive: 0.35 s
  • Avro conversion: 0.5 s
  • Send: 0.8 s
  • Cluster forward: 0.55 s
  • Deliver: 0.35 s
  • DLQ move: 0.6 s
  • HTTP request and response: 0.35 s each
  • SQL: 0.25 s (same region), 0.6 s (cross-region)
  • Replication: 0.15 s (same region), replLag (cross-region)
  • Read path: UI to Query API 0.6 s, Query API to API 0.7 s, and the same for the answer. These are slower than the rest, so that you can follow a query
—
Random seed7 (results repeat after Reset)—

6.3Scenario results

The read path results are in their own table.

These tables are the output of npm run scenarios -- --node on 2026-11-02. Each scenario has 60 simulated seconds of traffic at 2 events/s, if the scenario does not set a different rate. The run is seeded, so the numbers repeat. These scenarios run with the read path off, so they show ingestion only. Simulation

Messages

ScenarioPublishedPublish failedUnique doneDeliveries / eventDuplicatesSQS DLQArtemis DLQStuck in ArtemisArrivals B1/B2/B3Stored B1/B2/B3
1 Baseline: per-region queues12001201.00000052 / 8 / 040 / 40 / 40
2 One topic → both regions12001202.0012000038 / 31 / 5180 / 80 / 80
3 Same queue name, both consume12001202.0012000033 / 5 / 2240 / 40 / 40
4 Same queue name, us-east-1 only12001201.00000052 / 8 / 040 / 40 / 40
5 One topic + same name, both consume12001203.5630700038 / 0 / 8280 / 80 / 80
6 Poison 10% at Avro12001071.000130033 / 22 / 037 / 35 / 35
7 Poison 10% at the API (422)12001101.000010030 / 8 / 2240 / 40 / 40
8 Transient failures 20% (API 500)12001191.00001040 / 14 / 640 / 40 / 40
9 Slow consumers, kill B1 at 20 s, restart at 30 s18001781.00000428 / 26 / 3636 / 72 / 72
10 Same as 9, with redistribution18001801.00000028 / 26 / 3636 / 72 / 72
11 us-east-1 outage from 5 s to 15 s (global failover on)12001191.01101027 / 10 / 1340 / 40 / 38
12 Same as 11, with redistribution12001191.01101027 / 10 / 1340 / 40 / 38
13 us-east-1 outage, one topic120201002.0010001044 / 0 / 5668 / 66 / 65
14 New TCP connection for each message18001801.00000029 / 29 / 3260 / 60 / 60
15 Rate 8 events/s: HPA and writer ACUs48004801.00000060 / 108 / 72160 / 160 / 160
16 Bursts of 80 at 10 s and 30 s28002801.00000084 / 56 / 094 / 94 / 92
17 Same name, both consume, replica lag 2 s, read before write12001202.0012000030 / 5 / 2540 / 40 / 40
18 Rate 6, us-east-1 outage 20–30 s, global failover on36003541.01308089 / 22 / 38119 / 118 / 118
19 Rate 6, us-east-1 outage 20–30 s, global failover off36003471.000018088 / 46 / 15119 / 120 / 119
20 Kill e1-1 at 20 s, restart at 30 s, switchover at 35 s12001201.00000030 / 8 / 2240 / 40 / 40
21 Writer down 20–26 s (no replica), redelivery delay 012001121.00008052 / 8 / 040 / 40 / 40
22 Same as 21, redelivery delay 3 s12001201.00000033 / 27 / 040 / 40 / 40
23 Kill e2-1 at 20 s, restart at 25 s12001201.00000052 / 8 / 040 / 40 / 40
24 Kill e1-1 at 20 s, restart at 21 s, switchover at 22 s12001121.00008052 / 8 / 040 / 40 / 40
25 Burst 1000 in 1.5 s, capacity 10 / 10 / 101000010001.000000201 / 150 / 149334 / 334 / 332
26 Burst 1000 in 1.5 s, capacity 50 / 50 / 501000010001.000000161 / 219 / 120334 / 334 / 332

API and Aurora

ScenarioRows in AuroraDedupe at readDedupe at writeAPI 4xxAPI 5xxMax API pods e1 / e2Max writer ACURows lostAcked, no rowWriter at the endDrain time (s)Check
1 Baseline: per-region queues12000002 / 32.500us-east-1 (e1-1)68.7ok
2 One topic → both regions1200120003 / 4600us-east-1 (e1-1)67.6ok
3 Same queue name, both consume1200120003 / 5700us-east-1 (e1-1)69.8ok
4 Same queue name, us-east-1 only12000003 / 12.500us-east-1 (e1-1)68.7ok
5 One topic + same name, both consume1200307005 / 7800us-east-1 (e1-1)71.8ok
6 Poison 10% at Avro10700002 / 2400us-east-1 (e1-1)70.9ok
7 Poison 10% at the API (422)110003002 / 32.500us-east-1 (e1-1)69.5ok
8 Transient failures 20% (API 500)119000302 / 4400us-east-1 (e1-1)70.7ok
9 Slow consumers, kill B1 at 20 s, restart at 30 s17800002 / 3400us-east-1 (e1-1)98.5ok
10 Same as 9, with redistribution18000002 / 3400us-east-1 (e1-1)98.5ok
11 us-east-1 outage from 5 s to 15 s (global failover on)11901083 / 2400us-east-2 (e2-1)66.2ok
12 Same as 11, with redistribution11901083 / 2400us-east-2 (e2-1)66.2ok
13 us-east-1 outage, one topic1000100084 / 3600us-east-2 (e2-1)67.7ok
14 New TCP connection for each message18000003 / 4400us-east-1 (e1-1)69.3ok
15 Rate 8 events/s: HPA and writer ACUs48000005 / 6800us-east-1 (e1-1)107.9ok
16 Bursts of 80 at 10 s and 30 s28000005 / 8800us-east-1 (e1-1)72.2ok
17 Same name, both consume, replica lag 2 s, read before write1203585004 / 4600us-east-1 (e1-1)69.6ok
18 Rate 6, us-east-1 outage 20–30 s, global failover on352030288 / 5832us-east-2 (e2-1)90ok
19 Rate 6, us-east-1 outage 20–30 s, global failover off347010587 / 6800us-east-1 (e1-1)103.3ok
20 Kill e1-1 at 20 s, restart at 30 s, switchover at 35 s120000254 / 5400us-east-1 (e1-1)68.7ok
21 Writer down 20–26 s (no replica), redelivery delay 0112000373 / 52.500us-east-1 (e1-1)68.7ok
22 Same as 21, redelivery delay 3 s120000223 / 6400us-east-1 (e1-1)68.7ok
23 Kill e2-1 at 20 s, restart at 25 s12000002 / 32.500us-east-1 (e1-1)68.7ok
24 Kill e1-1 at 20 s, restart at 21 s, switchover at 22 s112000373 / 52.500us-east-1 (e1-1)68.7ok
25 Burst 1000 in 1.5 s, capacity 10 / 10 / 10100000008 / 9800us-east-1 (e1-1)213.1ok
26 Burst 1000 in 1.5 s, capacity 50 / 50 / 501000000048 / 50800us-east-1 (e1-1)63.7ok
  • Scenario 1: the arrivals are not equal (52 / 8 / 0), but the stored messages are equal (40 / 40 / 40). The NLB pins connections, and ON_DEMAND spreads the messages.
  • Scenario 9: 4 messages stay stranded on broker 1. This is two events, each with its mirror copy.
  • Scenario 11: the one duplicate is the failover edge condition. us-east-1 processed the message, but its ack did not get to us-east-2 before the outage. One message goes to the Artemis DLQ: three fast 503 responses during the promotion. At the end, the writer is e2-1, because the script does not do a switchover.
  • Scenario 13: 20 publishes fail. With one topic, the region of the topic is a single point of failure.
  • Scenarios 2, 3 and 5: each duplicate is removed at the write. The rows stay equal to the unique events.
  • Scenario 15: the writer gets to 8 ACU, the maximum. The API increases to 5 pods in us-east-1 and 6 pods in us-east-2.
  • Scenario 17: 35 duplicates stop at the read and 85 stop at the write. The read on a reader that is 2 s behind misses most duplicates.
  • Scenario 18: the global failover loses 3 rows (RPO). A later delivery of one of these events wrote its row again. The other 2 events have no row at the end, although a consumer acked them (acked, no row 2).
  • Scenario 19: with no global failover, each write gets 503 for the full outage. 18 messages go to the Artemis DLQ, but no rows are lost.
  • Scenarios 21 and 22: with redelivery delay 0, 8 messages go to the Artemis DLQ. With 3 s, no message does.
  • Scenario 23: with the read path off, the write-only API does not use e2-1. Thus a stopped reader has no effect on the requests.
  • Scenarios 25 and 26: the same spike of 1000 events. With capacity 10 / 10 / 10, the drain takes 212.7 s. With 50 / 50 / 50, it takes 68 s. At 50, the writer is at its maximum of 8 ACU, so Aurora is the next limit.

Artemis DLQ processing

These scenarios ran on 2026-11-02. With the processor on, each message that went to the Artemis DLQ is in S3 at the end. With it off, each one is still in the DLQ. Messages in the SQS DLQ always stay.

ScenarioSQS DLQMoved to Artemis DLQWaiting in Artemis DLQObjects in S3S3 overwritesDrain time (s)
27 Poison 10% + 10%, processor on9808072.2
28 Poison 10% + 10%, processor off9880072.2
29 Same, processor off until 30 s9808072.2
30 Poison 20% + 20%, us-east-1 outage 20–30 s2232032072.1
31 Poison 20% at the API, kill B1 at 20 s, restart at 30 s028028073.4

Read path

These rows are the output of the same command on 2026-11-03. Scenarios 1 to 31 run with the read path off, so the tables above stay the same. All 12 rows show ok in the column check. Last event done is the time until the last event finishes. Drain also includes the queries that wait in the Query API.

ScenarioUnique doneRowsQueries sentAnsweredFailedSkippedMax Query API pods e1/e2Max API pods e1/e2Max e2-1 ACULast event done (s)Drain (s)
32 Read path on, default rates120120120120004 / 43 / 41.567.468.2
33 Read path, heavy: Center 5/s, Cube 3/s per region12012047147104895 / 58 / 846891.1
34 Read path, heavy, API time 2 s: the UIs hit their open-query limit12012028028006805 / 510 / 10369.7121.6
35 Read path, us-east-1 outage 20–30s117115110106404 / 44 / 5467.668.4
36 Read path, kill us-east-2 reader @20s, restart @25s (nothing left to read there)120120120115504 / 43 / 41.567.468.2
37 Read path, kill us-east-1 reader @20s, restart @25s (reads fall back to the writer)120120120120004 / 43 / 41.567.468.2
38 Read path on until @30s, then ingestion only1201206060004 / 33 / 31.56868.7
39 API time 2 s, ingestion only (compare with 34)12012000001 / 14 / 50.569.269.9
40 Rate 6, API time 2 s, heavy reads36036028128106795 / 510 / 104135135.8
41 Rate 6, API time 2 s, ingestion only (compare with 40)36036000001 / 18 / 80.5134.7135.5
42 Same reads as 33, Query API max 20120120725725023520 / 2010 / 1066868.7
43 Same, Query API max 20, lowered to 5 @30s (pods above it drain, then stop)120120546546041420 / 2010 / 10667.484.8

Compare scenarios 34 and 39, and scenarios 40 and 41. The reads increase the API pods and the ACUs of the reader. They have almost no effect on the time until the last event finishes.

Mirrored queues

These rows are the output of the same command on 2026-11-03. All 8 rows show ok in the column check. In scenarios 50 and 51, us-east-1 does not come back. Stuck in Artemis then counts the messages on its brokers. With mirroring on, this count includes copies of messages that us-east-2 already processed.

ScenarioPublishedUnique doneDeliveries / eventDuplicatesMirror copiesStuck in ArtemisRowsRows lostLast event done (s)
44 Mirroring off1201201.00000120068
45 us-east-1 outage 20–30s, mirroring on1201171.0111330116168.2
46 us-east-1 outage 20–30s, mirroring off (compare with 45)1201161.00000115167.6
47 Same queue name, both regions consume, mirroring off (compare with 3)1201201.00000120068
48 Mirroring on until @30s, then off1201201.000440120068
49 Mirroring off until @30s, then on, us-east-1 outage @40–50s1201191.033880117367.4
50 Slow consumers, us-east-1 lost for good @20s, mirroring on1801691.000169161690163.2
51 Slow consumers, us-east-1 lost for good @20s, mirroring off (compare with 50)1801611.000081610157.6

Compare scenarios 45 and 46: a short outage has almost the same result, because the messages of us-east-1 wait and then finish. Compare scenarios 50 and 51: if the region does not come back, mirroring saves the messages that were in Artemis (169 and 161 events done). It does not save the messages that were in the SQS queue of that region. Compare scenarios 3 and 47: with no copies, one shared queue name gives no duplicates.

7Glossary

TermMeaning in this manual
AckAcknowledgement. A consumer sends it to tell the broker that it processed a message.
ACUAurora capacity unit. The capacity unit of Serverless v2: approximately 2 GiB of memory with matching CPU and network.
Address (Artemis)The destination to which producers send. Queues are bound to an address.
ALBApplication Load Balancer. A layer 7 AWS load balancer. It selects a target for each HTTP request.
AMQPAdvanced Message Queuing Protocol. The mirror connections use AMQP. The pods and consumers use Core.
APIThe HTTP service after the Artemis consumers. It stores each event in Aurora. Each region has one API Deployment with 1 to 10 pods (maximum 10 to 50 in the panel).
ArtemisApache ActiveMQ Artemis, the message broker software.
At-least-onceEach message is delivered one or more times. Duplicates are possible.
AuroraAmazon Aurora PostgreSQL, the database of the model.
AvroApache Avro, a compact binary format. A reader needs the schema of the writer.
BrokerOne Artemis server. Each region has three brokers: B1, B2 and B3.
Center, CubeThe two UIs of the read path. Each is a separate deployment. Cube is for analytics.
Cluster connection, core bridgeThe internal link that brokers in one cluster use to send messages to each other.
ConsumerAn Artemis client that receives messages from a queue. Each region has 10.
CoreThe protocol and client API of Artemis. The pods and consumers use it over TCP.
Cross-region deliveryAn SNS topic in one region that sends to an SQS queue in a different region.
Cross-region writeA write from the us-east-2 API to the writer in us-east-1. It pays for a cross-region round trip.
Dedupe at readThe API finds the row on its local reader and does not write. Only with Read before write on.
Dedupe at writeThe write INSERT … ON CONFLICT (event_id) DO NOTHING finds the row already on the writer and inserts nothing.
DLADead-letter address. The Artemis address that gets messages after max-delivery-attempts. The app calls it “Artemis DLQ”.
DLQDead-letter queue. A queue for messages that fail too many times.
DLQ processorA Deployment that moves messages from the Artemis DLQ to S3. It acks a message only after S3 sends 200.
DownstreamThe system that processes the message after the consumer receives it. In this model, the API.
DuplicateA second or later processing of the same event.
ENIElastic network interface. An interface endpoint is a set of ENIs.
Endpoint serviceThe PrivateLink service in the broker VPC. An NLB is in front of it.
EventThe item that the producer publishes. Each event has an event id.
Event idThe unique identifier of an event. The API uses it as the unique key (event_id) to remove duplicates.
Failover, failbackFailover: the other region takes over the standby copy. Failback: the original region consumes its own queue again.
Failover (Aurora)An unplanned change of the writer. In-region failover: Aurora promotes a reader in the same cluster, with no data loss. Global failover: Aurora promotes the secondary cluster. A global failover can lose the rows that did not replicate (RPO).
Fan-outSNS sends one copy of each message to each subscription.
Flow hashThe NLB calculation that selects one target for one TCP connection.
Global DatabaseAn Aurora feature that replicates one primary cluster to read-only secondary clusters in other regions, at the storage layer.
Global writer endpointThe Global Database endpoint that always points to the current writer, also after a switchover or a failover.
HPAHorizontalPodAutoscaler. The Kubernetes resource that sets the number of API pods from a metric: ⌈current × metric ÷ target⌉.
HTTP status 2xx, 4xx, 5xxThe API response classes. 2xx: success (200). 4xx: the request is not correct (422: the API always rejects the payload). 5xx: a server error (500: transient; 503: the database is not available).
Idempotent writeA write that gives the same result if you do it one time or many times. Here: INSERT … ON CONFLICT (event_id) DO NOTHING.
In flightAn SQS message that a pod received but did not delete. It is invisible.
In flight (API)A request that an API pod started but did not answer yet. Each pod has up to 2 in the app.
Interface endpointThe PrivateLink endpoint in the client VPC.
JolokiaAn HTTP interface to JMX data. The KEDA Artemis scaler reads the message count through it.
KEDAKubernetes Event-driven Autoscaling. It sets the number of pods from the queue length.
maxReceiveCountThe SQS redrive limit. A message with a higher ReceiveCount goes to the DLQ.
max-delivery-attemptsThe Artemis redelivery limit. Default 10.
MessageOne stored copy of an event in an SQS queue or in Artemis.
MirrorAn Artemis AMQP broker connection that copies sends and acks to a broker in the other region. “Dual mirror” means in the two directions.
Mirror backlogMirror events that wait because the target is down.
MULTICASTAn Artemis routing type. Each bound queue gets a copy of each message.
NLBNetwork Load Balancer. An AWS layer 4 (TCP) load balancer.
ON CONFLICT … DO NOTHINGA PostgreSQL INSERT clause. If a row with the same unique key exists, the insert does nothing and causes no error. It needs a unique index or constraint.
ON_DEMANDThe default value of message-load-balancing. Messages go round-robin to brokers whose queue has consumers.
PodA Kubernetes pod that receives from SQS, converts to Avro and sends to Artemis.
Poison messageA message that fails on each attempt.
PullThe client asks for a message, and the answer carries it. SQS works in this way.
PushThe broker sends a message to a connected client with no ask for each message. Artemis works in this way.
Primary clusterThe Aurora cluster that holds the writer. Here: e1-aurora in us-east-1, with e1-1 and e1-2.
PrivateLinkAn AWS service for private connections between VPCs.
ProducerThe client that publishes events to SNS.
Promotion tierThe priority of an Aurora reader for a failover, from 0 (first) to 15. A Serverless v2 reader in tier 0 or 1 keeps at least the capacity of the writer.
queueLengthThe KEDA target number of messages for each pod.
Query APIThe API between the two UIs and the existing API. It only reads. Each region has one Deployment with 1 to 5 pods (HPA; maximum 5 to 50 in the panel).
Raw message deliveryAn SNS setting that removes the SNS JSON envelope.
Read before writeA panel switch (readCheck, default off). On: the API reads its local reader before it writes.
Read pathCenter, Cube, the Query API and their queries. Set Show to hide it.
ReaderAn Aurora instance that serves only reads: e1-2 and e2-1 at the start.
redelivery-delayThe Artemis time before a rolled-back message is delivered again. Default 0.
Redistribution, redistribution-delayArtemis moves stored messages off a broker whose queue has no consumers. −1 means never.
RedriveThe SQS move of a message to its DLQ.
RegionAn AWS region: us-east-1 (N. Virginia) or us-east-2 (Ohio). In part ids, e1 and e2.
Replica lagThe time from a commit on the writer until the row is visible on a reader. In the same cluster: milliseconds. To the secondary region: typically less than 1 s.
RollbackA consumer tells the broker that processing failed. The broker then delivers the message again.
RPORecovery point objective. The quantity of committed data that a failover can lose. Here: the rows that did not replicate to e2-1.
RTORecovery time objective. The time until the system accepts writes again after a failure.
S3Amazon Simple Storage Service, an object store. Each region has one bucket for messages from the Artemis DLQ.
ScaledObjectThe KEDA resource that connects a scaler to a workload.
Schema registryA service that stores Avro schemas. A record carries the schema id.
Secondary clusterA read-only Aurora cluster in another region of the Global Database. Here: e2-aurora in us-east-2, with e2-1.
Serverless v2An Aurora capacity mode. Each instance scales between a minimum and a maximum number of ACUs, in steps as small as 0.5 ACU.
SNSAmazon Simple Notification Service. A publish-subscribe service.
SQSAmazon Simple Queue Service. A message queue service.
Stabilization windowThe HPA time before a scale-down. The HPA uses the highest recommendation in this window. Default 300 s; 10 s in the app.
Standby copyThe mirror copy of the queue of the other region. Nobody reads it until a failover.
SwitchoverA planned change of the Aurora writer. It waits until the target is up to date, so it loses no data (RPO 0).
sync=trueA mirror option. The client waits until the mirror has the copy.
Transient failureA failure that occurs at random on one attempt. A retry usually succeeds.
use1.*, use2.*The address and queue names of us-east-1 and us-east-2.
Visibility timeoutThe time for which SQS hides a received message.
VPCVirtual Private Cloud. A private network in AWS.
Write forwardingAn Aurora Global Database option. A reader in a secondary cluster sends the writes that it receives to the writer.
WriterThe one Aurora instance that accepts writes: e1-1 at the start.

8Sources

  1. Apache ActiveMQ Artemis. AMQP broker connections and mirror. artemis.apache.org
  2. The same page, earlier location. activemq.apache.org
  3. Amazon SNS. Cross-region delivery. docs.aws.amazon.com
  4. Amazon SNS. Raw message delivery; delivery retries; dead-letter queues.
  5. Amazon SQS. Standard queues. docs.aws.amazon.com
  6. Amazon SQS. Dead-letter queues; visibility timeout; SetQueueAttributes.
  7. AWS PrivateLink. Endpoint services. docs.aws.amazon.com
  8. Elastic Load Balancing. NLB introduction; Network Load Balancers; how Elastic Load Balancing works.
  9. KEDA. aws-sqs-queue scaler; ScaledObject specification; Artemis scaler.
  10. Apache Avro 1.12 specification. avro.apache.org
  11. Confluent Schema Registry. Wire format. docs.confluent.io
  12. Apache ActiveMQ Artemis. Address model (MULTICAST, ANYCAST) and mirror notes. artemis.apache.org
  13. Apache ActiveMQ Artemis. Clusters: load balancing and redistribution. artemis.apache.org
  14. Apache ActiveMQ Artemis. Address settings: dead-letter address and max-delivery-attempts. activemq.apache.org
  15. Kubernetes. Horizontal Pod Autoscaling: sync period, algorithm, stabilization window and tolerance. kubernetes.io (examined 2026-10-23)
  16. Amazon Aurora. How Aurora Serverless v2 works: ACU, 0.5-ACU steps, capacity ranges and promotion tiers. docs.aws.amazon.com (examined 2026-10-23)
  17. Amazon Aurora. Serverless v2 auto-pause (scaling to 0 ACU). docs.aws.amazon.com (linked from [16])
  18. Amazon Aurora. Using Amazon Aurora Global Database: storage-level replication, lag less than 1 s, read-only secondaries, up to 10. docs.aws.amazon.com (examined 2026-10-23)
  19. Amazon Aurora. Global Database switchover and failover: RPO, RTO and rds.global_db_rpo. docs.aws.amazon.com (examined 2026-10-23)
  20. Amazon Aurora. Write forwarding in an Aurora PostgreSQL global database. docs.aws.amazon.com (examined 2026-10-23)
  21. Amazon Aurora. High availability: in-region failover and promotion tiers. docs.aws.amazon.com (examined 2026-10-23)
  22. PostgreSQL. INSERT … ON CONFLICT. postgresql.org (examined 2026-10-23)
  23. Amazon Aurora. Replication with Amazon Aurora: replica lag usually much less than 100 ms. docs.aws.amazon.com (examined 2026-10-23)
  24. Amazon S3. Data consistency model.
  25. Elastic Load Balancing. Target group attributes for an Application Load Balancer.
  26. Amazon SQS. ReceiveMessage.
  27. Apache ActiveMQ Artemis. Flow control; Core.
  28. Apache ActiveMQ Artemis. Detecting dead connections.

Project files: research/notes.md (research notes and numbers), README.md (scenarios, controls, deploy), CONTRACT.md (design of the API and Aurora parts), src/content/topic.js (tour, panel, legend), src/content/info.js (parts), src/content/sim.js (simulation rules and timings) and scripts/scenarios.mjs (scenario list).