<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Felipe Malaquias</title>
    <description>The latest articles on DEV Community by Felipe Malaquias (@malaquf).</description>
    <link>https://dev.to/malaquf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1256889%2F7bda1a7b-3456-49d0-ab98-b080e8eeae30.jpeg</url>
      <title>DEV Community: Felipe Malaquias</title>
      <link>https://dev.to/malaquf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/malaquf"/>
    <language>en</language>
    <item>
      <title>Using Docker in AWS Amplify Builds: A Step-by-Step Guide</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Sun, 27 Apr 2025 20:14:01 +0000</pubDate>
      <link>https://dev.to/aws-builders/using-docker-in-aws-amplify-builds-a-step-by-step-guide-4mn6</link>
      <guid>https://dev.to/aws-builders/using-docker-in-aws-amplify-builds-a-step-by-step-guide-4mn6</guid>
      <description>&lt;p&gt;After nearly a year of happily using Amplify Gen2, I started facing problems as soon as I added a Docker image asset to my project.&lt;/p&gt;

&lt;p&gt;I had something along these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const cluster = new ecs.Cluster(stack, 'ECSCluster', {
  clusterName: 'ECSCluster',
  vpc: vpc,
});

const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDefinition', {
  cpu: 4096,
  memoryLimitMiB: 16384,
  runtimePlatform: {
    cpuArchitecture: ecs.CpuArchitecture.X86_64,
    operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
  },
})

const dockerImageAsset = new DockerImageAsset(stack, 'DockerImageAsset', {
  directory: resolvePath('./docker'),
  platform: Platform.LINUX_AMD64,
});
// see https://aws.amazon.com/blogs/aws/aws-fargate-enables-faster-container-startup-using-seekable-oci/
// and https://github.com/aws/aws-cdk/issues/26413
SociIndexBuild.fromDockerImageAsset(stack, 'Index', imageProcessorDockerImage);

taskDefinition.addContainer('Container', {
  containerName: 'Container',
  image: ecs.ContainerImage.fromDockerImageAsset(imageProcessorDockerImage),
  essential: true,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as I added this to my Amplify Gen2 + CDK project, my build started to fail without any clear error message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2025-04-27T08:46:25.054Z [INFO]: 8:46:25 AM Building and publishing assets...
2025-04-27T08:46:26.340Z [INFO]: 
2025-04-27T08:46:26.341Z [WARNING]: ampx pipeline-deploy
                                    Command to deploy backends in a custom CI/CD pipeline. This command is not inten
                                    ded to be used locally.
                                    Options:
                                    --debug            Print debug logs to the console
                                    [boolean] [default: false]
                                    --branch           Name of the git branch being deployed
                                    [string] [required]
                                    --app-id           The app id of the target Amplify app[string] [required]
                                    --outputs-out-dir  A path to directory where amplify_outputs is written. I
                                    f not provided defaults to current process working dire
                                    ctory.                                         [string]
                                    --outputs-version  Version of the configuration. Version 0 represents clas
                                    sic amplify-cli config file amplify-configuration and 1
                                    represents newer config file amplify_outputs
                                    [string] [choices: "0", "1", "1.1", "1.2", "1.3", "1.4"] [default: "1.4"]
                                    --outputs-format   amplify_outputs file format
                                    [string] [choices: "mjs", "json", "json-mobile", "ts", "dart"]
                                    -h, --help             Show help                                     [boolean]
2025-04-27T08:46:26.341Z [INFO]: 
2025-04-27T08:46:26.342Z [INFO]: [CDKAssetPublishError] CDK failed to publish assets
                                  ∟ Caused by: [_ToolkitError] Failed to publish asset data Nested Stack Template (current_account-current_region)
                                  Resolution: Check the error message for more details.
2025-04-27T08:46:26.342Z [INFO]: 
2025-04-27T08:46:26.343Z [INFO]: 
2025-04-27T08:46:26.345Z [INFO]: 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Appending a debug flag to the amplify deploy command helped to identify Docker was not available in the path, although hidden through an INFO message somewhere in the middle of the logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: 1
backend:
  phases:
    build:
      commands:
        - npm i
        - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID --debug
frontend:
  phases:
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: dist
    files:
      - '**/*'
  cache:
    paths:
      - .npm/**/*
      - node_modules/**/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2025-04-27T10:15:35.435Z [INFO]: 10:15:35 AM [deploy: CDK_TOOLKIT_E0000] 10:15:35 AM amplify-main-branch: fail: Unable to execute 'docker' in order to build a container asset. Please install 'docker' and try again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, we need to use a different image compatible with AWS CodeBuild. A list of official AWS CodeBuild curated Docker images can be found &lt;a href="https://github.com/aws/aws-codebuild-docker-images?tab=readme-ov-file" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By scrolling down in the Hosting/Build settings area, it is possible to set a custom image, such as &lt;em&gt;public.ecr.aws/codebuild/amazonlinux-x86_64-standard:5.0, *which contains Docker&lt;/em&gt;.*&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmlvsuz39q7m59r0kr1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmlvsuz39q7m59r0kr1k.png" alt="Using custom image in build settings" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After switching to the image mentioned above, it is still necessary to start &lt;em&gt;dockerd&lt;/em&gt; by running the &lt;em&gt;/usr/local/bin/dockerd-entrypoint.sh&lt;/em&gt; script before building with Docker in your amplify.yml, like the example below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: 1
backend:
  phases:
    build:
      commands:
        - /usr/local/bin/dockerd-entrypoint.sh      
        - npm i
        - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID --debug
frontend:
  phases:
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: dist
    files:
      - '**/*'
  cache:
    paths:
      - .npm/**/*
      - node_modules/**/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And voilà! Your project should now be able to build the Docker image successfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thanks for reading!&lt;/strong&gt; Do you have any cool ideas or feedback you'd like to share? Please drop a comment, send me a message, or &lt;a href="https://www.linkedin.com/in/fmalaquias/" rel="noopener noreferrer"&gt;follow me&lt;/a&gt;, and let’s keep building!&lt;/p&gt;

</description>
      <category>amplify</category>
      <category>aws</category>
      <category>codebuilder</category>
      <category>docker</category>
    </item>
    <item>
      <title>Automate Email Processing using Event Driven Architecture and Generative AI</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Wed, 05 Feb 2025 03:10:17 +0000</pubDate>
      <link>https://dev.to/aws-builders/automate-email-processing-using-event-driven-architecture-and-generative-ai-27gj</link>
      <guid>https://dev.to/aws-builders/automate-email-processing-using-event-driven-architecture-and-generative-ai-27gj</guid>
      <description>&lt;p&gt;Recently, I came up with a use case for the application I am currently working on where I wanted to automate email processing in order to extract key information in a structured format I could then use later on in other internal processes. To achieve this with the least possible cost in AWS while maintaining scalability and efficiency, I implemented the following architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hwbdi6elsu1x6q2c6co.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hwbdi6elsu1x6q2c6co.png" alt="Event Driven Architecture for Email Processing" width="688" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am using &lt;a href="https://aws.amazon.com/workmail/" rel="noopener noreferrer"&gt;SES&lt;/a&gt; as an entry point for the emails. SES will then store the raw email in &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-s3.html" rel="noopener noreferrer"&gt;S3 bucket&lt;/a&gt; according to the rules defined in the CDK stack below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const bucket = new Bucket(sesStack, 'SesBucket',
  {
    bucketName: `my-bucket-${process.env.AWS_BRANCH}`,
    publicReadAccess: false,
    removalPolicy: RemovalPolicy.DESTROY,
    intelligentTieringConfigurations: [
      {
        name: 'SES-Intelligent-Tiering',
        archiveAccessTierTime: Duration.days(90),
        deepArchiveAccessTierTime: Duration.days(365),
      },
    ],
    eventBridgeEnabled: true,
  }
);

new ReceiptRuleSet(sesStack, 'SesRuleSet', {
  rules: [
    {
       recipients: ['myemail@myaddress.com'],
       actions: [
         new S3({
           bucket,
           objectKeyPrefix: 'emails/raw',
         }),
       ],
       scanEnabled: true,
    },
 ],
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Because the code above enables &lt;a href="https://aws.amazon.com/eventbridge/" rel="noopener noreferrer"&gt;EventBridge&lt;/a&gt; events on the bucket, we can then create a new EventBridge &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rules.html" rel="noopener noreferrer"&gt;rule&lt;/a&gt; to trigger a &lt;a href="https://aws.amazon.com/step-functions/" rel="noopener noreferrer"&gt;StepFunction&lt;/a&gt; that will then process the emails as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const cleanseEmailFunction = new NodejsFunction(stack, 'CleanseEmailFunction', {
  ...getCommonLambdaProps(),
  entry: './amplify/functions/cleanseEmail.ts'
})
bucket.grantReadWrite(cleanseEmailFunction)

const extractDataFunction = new NodejsFunction(stack, 'ExtractDataFunction', {
  ...getCommonLambdaProps(),
  entry: './amplify/functions/extractDataFunction.ts',
})
table.grantReadWriteData(extractDataFunction)
bucket.grantRead(extractDataFunction)

const stateMachine = new StateMachine(stack, 'EmailProcessingStateMachine', {
  definitionBody: DefinitionBody.fromFile('./amplify/step-functions/processEmails.asl.json'),
  timeout: Duration.minutes(29),
  tracingEnabled: true,
  stateMachineType: StateMachineType.STANDARD,
  logs: {
    level: LogLevel.ALL,
    destination: new LogGroup(stack, 'ProcessEmailsStateMachineLogs', {
    logGroupName: '/aws/vendedlogs/states/ProcessEmailsStateMachine',
    retention: RetentionDays.ONE_WEEK,
  }),
  includeExecutionData: true,
},
definitionSubstitutions: {
  TableName: table.tableName,
  CleanseEmailFunction: cleanseEmailFunction.functionName,
  ExtractDataFunction: extractDataFunction.functionName,
},
comment: 'State machine to process email'
});

extractDataFunction.grantInvoke(stateMachine);
cleanseEmailFunction.grantInvoke(stateMachine);

const rule = new Rule(stack, 'EmailS3ObjectCreatedRule', {
  eventPattern: {
    source: ['aws.s3'],
    detailType: ['Object Created'],
    detail: {
      bucket: {
        name: [bucket.bucketName]
      },
      object: {
        key: [
          {
            prefix: "emails/raw/"
          },
        ]
      }
   }
}
});

rule.addTarget(new SfnStateMachine(stateMachine));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This state machine is composed of basically two functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CleanseEmailFunction:&lt;/strong&gt; removes all sensitive data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ExtractDataFunction:&lt;/strong&gt; uses &lt;a href="https://www.langchain.com/" rel="noopener noreferrer"&gt;Langchain&lt;/a&gt; and &lt;a href="https://www.langchain.com/langsmith" rel="noopener noreferrer"&gt;LangSmith&lt;/a&gt; to validate and extract structured JSON info through &lt;a href="https://aws.amazon.com/bedrock/" rel="noopener noreferrer"&gt;Bedrock&lt;/a&gt; and &lt;a href="https://aws.amazon.com/bedrock/claude/" rel="noopener noreferrer"&gt;Sonnet 3.5 v2&lt;/a&gt; and then store it in &lt;a href="https://aws.amazon.com/dynamodb/" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt; for later use&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Later, I could use this data to perform summarization and analytics, send tailored push notifications to users, and so on.&lt;/p&gt;

&lt;p&gt;This straightforward architecture to automate email processing can be extended as needed to perform additional tasks, like evaluating the model response and performing further transformations if needed. The best of all is that I pay only for what I use.&lt;/p&gt;

&lt;p&gt;If you want to scale it further and optimize costs, you may add a queue with &lt;a href="https://aws.amazon.com/pt/sqs/" rel="noopener noreferrer"&gt;SQS&lt;/a&gt; in between and use &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/batch-inference.html" rel="noopener noreferrer"&gt;batch inference&lt;/a&gt; to process emails at once.&lt;/p&gt;

&lt;p&gt;Please notice that batch inference may not be enabled in your account, and you may need to request it through support, though (I’ve been fighting with support to get access for more than a month now — with a business support plan).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Thanks for reading!&lt;/strong&gt; Got any cool ideas or feedback you want to share? Drop a comment, send me a message, or &lt;a href="https://www.linkedin.com/in/fmalaquias/" rel="noopener noreferrer"&gt;follow me,&lt;/a&gt; and let’s keep building!&lt;/p&gt;

</description>
      <category>bedrock</category>
      <category>rag</category>
      <category>ses</category>
      <category>stepfunctions</category>
    </item>
    <item>
      <title>Migrating From Redis to Valkey Serverless</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Mon, 27 Jan 2025 01:58:04 +0000</pubDate>
      <link>https://dev.to/aws-builders/migrating-from-redis-to-valkey-serverless-7f1</link>
      <guid>https://dev.to/aws-builders/migrating-from-redis-to-valkey-serverless-7f1</guid>
      <description>&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;Summer last year we started refactoring one of our services in order to get rid of I/O blocking operations. It was almost a complete rewrite of the service in about 2 months. &lt;br&gt;
After running the updated service for some time, we found out the hard way Elasticache Redis’ &lt;a href="https://repost.aws/pt/questions/QU2iFKoyuFSp2OFBNhsBg35g/elasticache-redis-network-bandwidth-in-out-allowance-exceeded" rel="noopener noreferrer"&gt;maximum allowance bandwidth&lt;/a&gt; constraints during weeks of intense traffic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F9162%2F1%2AVP9OchB395GEBw6c2tpOJg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F9162%2F1%2AVP9OchB395GEBw6c2tpOJg.png" alt="Sudden increase of network bytes in/out exceeding maximum nodes bandwidth" width="800" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The traffic doubled in a very short period of time and exceeded the maximum bandwidth allowance for one of our Redis nodes for a &lt;strong&gt;sustained&lt;/strong&gt; period of time. When this happens, queue increases and AWS starts to drop packets. There were a couple of issues that led this to cause a cascade effect, practically putting our service down for almost one hour while we identified the problem, scaled the nodes accordingly and waited for the cluster to balance.&lt;/p&gt;

&lt;p&gt;Notice the “sustained” wording in the paragraph above.&lt;/p&gt;

&lt;p&gt;Despite the fact we ran load tests in this service, we were not able to see this issue before because the test only ran from 10 to 30min maximum, and Elasticache allows you to exceed the network baseline for some undetermined period of time (up to an hour if I am not mistaken) until it starts to drop packets.&lt;/p&gt;

&lt;p&gt;There we had a couple of problems leading to the downtime:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem #1:&lt;/strong&gt; during the refactoring, we missed to set the default timeout for the Redis client, leading it to wait for 60 seconds (default) before timing out, causing our clients to timeout first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem #2:&lt;/strong&gt; during the refactoring, we also missed to migrate our circuit breaker implementation to our custom cache handling, and therefore, never skipping the cluster and going directly to the DB and performing requests normally as it should (notice here the cache in this case is used for answering fast to clients, and the DB should always be sized accordingly in order to handle normal load if caching is not present for any reason).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem #3:&lt;/strong&gt; during the rewriting, we optimised our startup time to a few seconds by removing a local cache in memory for one of our data structures. The issue we didn’t realise is that by doing so, we created a hot shard in our cluster, because the key for such lookups were not hashed and the values were basically always the same for all requests, leading to the load to not be distributed over our shards and therefore consuming more bandwidth from one of its nodes and making it not horizontally scalable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem #4:&lt;/strong&gt; because we could not predict this sudden load accordingly, our Elasticache cluster was not sized accordingly (e.g: by increasing number of shards) and therefore hit the network limitation even though the cluster seemed healthy at a first glance (CPU and MEM).&lt;/p&gt;

&lt;p&gt;The mitigation in this case was to simply scale the cluster vertically and increase the number of shards, and the final solution was to identify and fix each one of the points mentioned above in the next days.&lt;/p&gt;

&lt;p&gt;After that, we knew AWS offered a Serverless version of Elasticache which we planned to have a look at for some time already, and we’ve just had heard about Valkey, &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/10/amazon-elasticache-valkey/" rel="noopener noreferrer"&gt;announced&lt;/a&gt; about 1 month prior to this incident.&lt;/p&gt;
&lt;h2&gt;
  
  
  Elasticache Valkey
&lt;/h2&gt;

&lt;p&gt;Valkey is an opensource project forked from the open source Redis project right before the transition to their new source available licenses. Because of the transition, AWS and other tech giants started to contribute for the project, looking to keep Redis compatibility while enhance overall functionality and performance.&lt;/p&gt;

&lt;p&gt;The highlights from my point of view and experience so far are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;lower price than other engines (up to 33% lower)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it provides microseconds read and write latency and can scale to 500 million requests per second (RPS) on single self-designed (node-based) cluster&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it is compatible with Redis OSS APIs and data formats&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;zero downtime migration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;continuous updates (in exchange with some of the people involved in the project, they opened up ideas and plans to improve the service further, which will make it even more attractive in the future)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Valkey is offered in both cluster and serverless variants.&lt;/p&gt;

&lt;p&gt;You can read about all its bells and whistles at official AWS documentation and also see how it works in this video presented by one of the maintainers last summer:&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/tJZUVkMBdcg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Serverless Elasticache Valkey
&lt;/h2&gt;

&lt;p&gt;In addition to the highlights mentioned above, the serverless variant abstracts the cluster managing (minor updates) and sizing, which is very interesting in special if your traffic can suddenly change as pictured in the beginning of this article.&lt;/p&gt;

&lt;p&gt;Of course, not everything is flowers. The serverless variant may become very expensive depending on your workload and if you have a predictable and sustained load, you’d be probably paying much more for the serverless variant than the self-designed cluster one.&lt;/p&gt;

&lt;p&gt;In the serverless variant, you are billed by Storage and ECPU (ElastiCache Processing Units). Storage is straight forward and you can already estimate it based on your current values. ECPU is however a bit more tricky as it is basicallly the processing time, which is affected by the payload size and type of commands you execute. In general, 1 ECPU relates to approx. 1kb of payload data (read &lt;a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/WhatIs.corecomponents.html" rel="noopener noreferrer"&gt;this&lt;/a&gt; for more information).&lt;/p&gt;

&lt;p&gt;However, the very cool things about the serverless variant are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If your workload is periodical or irregular, you might save on costs during low usage periods&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ElastiCache Serverless for Valkey can &lt;a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/WhatIs.corecomponents.html" rel="noopener noreferrer"&gt;double the supported requests per second (RPS) every 2–3 minutes&lt;/a&gt;, reaching 5M RPS per cache from zero in under 13 minutes, with consistent sub-millisecond p50 read latency&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take the following metrics as an example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc15wthlzvcpmvp7ncnz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc15wthlzvcpmvp7ncnz0.png" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vfbkdukljx0tsbfcm1g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vfbkdukljx0tsbfcm1g.png" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The red and yellow areas in some of those panels are set only for cost control purposes, but the cluster is able to scale much more than that. There we can see traffic doubling in a very short amount of time and no throttles observed. Elasticache Valkey Serverless scales seamslessly in order to support such traffic increase, and downscales to the minimum configured ECPU value when traffic decreases.&lt;/p&gt;

&lt;p&gt;Because our service traffic has such sinusoidal look and the amount of data we transfer per second under normal load is not very high, we end up saving on costs with the benefit of it autoscaling as it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Careful considerations
&lt;/h2&gt;

&lt;p&gt;Before deciding to switch to Elasticache Serverless, analyse careful your workload and the AWS documentation in order to identify for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;is your cache traffic predictable and relatively constant or it is periodical or unpredictable?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how much ECPU your application would consume under normal load?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how much storage your application requires?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;do you need/want to set a minimum and/or maximum ECPU/s?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;do you need/want to set a minimum and/or maximum storage?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be aware, for example, that if you set maximum constraints, your application might receive errors from Elasticache when it surpasses such values, instead of scaling. However, setting limits might be a good idea if your application can tolerate errors and want to avoid excessive costs (e.g.: bypassing cache with circuit breakers and low client timeouts).&lt;/p&gt;

&lt;p&gt;Setting minimum values, on the other hand, could be a good idea to guarantee that your Elasticache will serve at least that amount of data at any given time.&lt;/p&gt;

&lt;p&gt;Use &lt;a href="https://aws.amazon.com/elasticache/pricing/" rel="noopener noreferrer"&gt;AWS’ pricing calculator&lt;/a&gt; to estimate how much it would cost you and make the better decision for your own use case.&lt;/p&gt;

&lt;p&gt;Make sure to also double check your security group rules, as the serverless variant requires the 6380 port for the reader nodes in addition to the standard 6379, otherwise, your application might start but you may experience latency. Read more &lt;a href="https://medium.com/@felipemalaquias/migrating-from-redis-to-valkey-serverless-0d3f3e8cffe7" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Good luck!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thanks for reading!&lt;/strong&gt; Got any cool ideas or feedback you want to share? Drop a comment, send me a message or &lt;a href="https://www.linkedin.com/in/fmalaquias/" rel="noopener noreferrer"&gt;follow me&lt;/a&gt; and let’s keep moving things forward!&lt;/p&gt;

</description>
      <category>valkey</category>
      <category>elasticache</category>
      <category>serverless</category>
    </item>
    <item>
      <title>How did I contribute for OpenAI’s Xmas Bonus before cutting 50% costs while scaling 10x with GenAI processing</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Tue, 24 Dec 2024 13:40:04 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-did-i-contribute-for-openais-xmas-bonus-before-cutting-50-costs-while-scaling-10x-with-genai-10b7</link>
      <guid>https://dev.to/aws-builders/how-did-i-contribute-for-openais-xmas-bonus-before-cutting-50-costs-while-scaling-10x-with-genai-10b7</guid>
      <description>&lt;p&gt;Yet another screw-up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; use OpenAI’s &lt;a href="https://platform.openai.com/docs/api-reference/files" rel="noopener noreferrer"&gt;Files&lt;/a&gt; and &lt;a href="https://platform.openai.com/docs/api-reference/batch" rel="noopener noreferrer"&gt;Batch&lt;/a&gt; APIs for async non-time-sensitive processing. Code below.&lt;/p&gt;

&lt;p&gt;I screw up so often that I actually learned to love the process of screwing up!&lt;/p&gt;

&lt;p&gt;Please don’t confuse it with being reckless, though. Think of it as a fast incremental learning process, just like fine-tuning a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Most Recent Screw-Up
&lt;/h2&gt;

&lt;p&gt;At the beginning of this year (2024), I created my first automation using GenAI for prototyping a travel app I used for giving a talk at the AWS User Group Berlin meetup showing case the new Amplify Gen 2 services (&lt;a href="https://www.youtube.com/watch?v=uIh3cI3FgTg" rel="noopener noreferrer"&gt;link here&lt;/a&gt;). At that time, unfortunately, there was only one way to generate chat completions using OpenAI’s &lt;a href="https://platform.openai.com/docs/guides/text-generation" rel="noopener noreferrer"&gt;*/v1/chat/completions&lt;/a&gt;* API.&lt;/p&gt;

&lt;p&gt;Recently, I needed to automate another process for which GenAI was a good fit. Therefore, I reused the same approach I had before and created the following state machine using AWS Step Functions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foge78gm4ar8jd2blvi03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foge78gm4ar8jd2blvi03.png" alt="Step Function iterating over DB records for assembling GenAI prompts and invoking OpenAI chat completion" width="424" height="1779"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I somehow have a fetish for diagrams, so I found myself proud and smart after I finished it - a feeling that didn’t last that long until I realized I had neither the money for my brother’s Xmas gift nor was I smart.&lt;/p&gt;

&lt;p&gt;Without going into details on the workflow itself, there are mainly two issues with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Although step functions are great for automation, they have their limitations (e.g.: maximum number of history events — steps— of 25000)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS offers 4000 free state transitions as part of their free tear. Anything above that, you pay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;APIs are &lt;a href="https://platform.openai.com/docs/guides/rate-limits" rel="noopener noreferrer"&gt;rate-limited&lt;/a&gt; in order to avoid abuse. OpenAI is, of course, no different (especially as it is being explored by so many people around the world right now). Therefore, it greatly restricts your ability to parallelize.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GenAI is generally still very slow considering low latency APIs all around us nowadays, especially if you require more complex models like &lt;a href="https://openai.com/o1/" rel="noopener noreferrer"&gt;o1&lt;/a&gt;. Therefore, if you need to iterate over 25000 prompts without &lt;a href="https://platform.openai.com/docs/guides/fine-tuning" rel="noopener noreferrer"&gt;fine-tuning&lt;/a&gt; your model, you will see yourself waiting more than 6 hours for it to complete and eventually failing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Models like o1 are still &lt;a href="https://openai.com/api/pricing/" rel="noopener noreferrer"&gt;relatively expensive&lt;/a&gt; when a large number of tokens are requested.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, after my first frustrated run for my new use case, I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Waited for 05:45:36.299 hours until the step function failed with a runtime error: &lt;em&gt;The execution reached the maximum number of history events (25000).&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spent $90 on tokens&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exceeded my free 4000 state transitions limit in my AWS account&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu5byrfsc1601kmv1pdi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu5byrfsc1601kmv1pdi.png" alt="Step Function Failure after more than 5h…" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;As I was, of course, in disbelief there wouldn’t be a better way to achieve what I wanted, I started re-reading the documentation.… to my happy surprise, I see a shiny new &lt;a href="https://platform.openai.com/docs/guides/batch" rel="noopener noreferrer"&gt;batch API&lt;/a&gt; I had overlooked, &lt;a href="https://community.openai.com/t/batchapi-is-now-available/718416" rel="noopener noreferrer"&gt;launched in April&lt;/a&gt; this year (2024).&lt;/p&gt;

&lt;p&gt;So I wrote the following lambda (in typescript) instead:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import OpenAI, { toFile } from 'openai';
import { BatchWriteCommand, BatchWriteCommandInput, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient, GetItemCommand, QueryCommand } from '@aws-sdk/client-dynamodb';
import { v4 as uuidv4 } from 'uuid';
import { GenerateTipsEvent } from '../shared/types/tips';
import { FileLike } from 'openai/uploads.mjs';
import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions.mjs';

// ParamsAndSecretsLayerVersion used for secrets retrieval/caching
const AWS_SECRETS_EXTENTION_SERVER_ENDPOINT = "http://localhost:2773/secretsmanager/get?secretId="

let openai: OpenAI | undefined;

const BATCH_REQUESTS_TABLE = process.env.BATCH_REQUESTS_TABLE || 'BatchRequests';

const ddbClient = new DynamoDBClient({});
const ddb = DynamoDBDocumentClient.from(ddbClient);

// one single request within the batch
interface BatchRequestLineItem {
  custom_id: string;
  method: string;
  url: string;
  body: ChatCompletionCreateParamsNonStreaming;
}

interface BatchRequestInput {
  lineItem: BatchRequestLineItem;
  custom_id: string;
  // any other field you may want use later in post processing
}

// split db command chunks to avoid exceeding max limits
const chunk = &amp;lt;T&amp;gt;(arr: T[], size: number): T[][] =&amp;gt; {
  return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =&amp;gt;
    arr.slice(i * size, i * size + size)
  );
};

async function initOpenAi() {
  if (!openai) {
    const openAiSecret = JSON.parse(await getSecretValue(process.env.OPEN_AI_SECRET_NAME!))

    openai = new OpenAI({
      apiKey: openAiSecret.apiKey,
      organization: openAiSecret.orgId,
    });
  }
}

const getSecretValue = async (secretName: string) =&amp;gt; {
  const url = `${AWS_SECRETS_EXTENTION_SERVER_ENDPOINT}${secretName}`;
  const response = await fetch(url, {
    method: "GET",
    headers: {
      "X-Aws-Parameters-Secrets-Token": process.env.AWS_SESSION_TOKEN!,
    },
  });

  if (!response.ok) {
    throw new Error(
      `Error occured while requesting secret ${secretName}. Responses status was ${response.status}`
    );
  }

  const secretContent = (await response.json()) as { SecretString: string };
  return secretContent.SecretString;
};

const getTopicsForCategory = async (categoryId: string) =&amp;gt; {
  const result = await ddb.send(new QueryCommand({
    // ... boring stuff
  }));
  return result.Items || [];
};

const getSectionsForTopic = async (topicId: string) =&amp;gt; {
  const result = await ddb.send(new QueryCommand({
    // ... boring stuff
  }));
  return result.Items || [];
};

const getUnitsForSection = async (sectionId: string) =&amp;gt; {
  const result = await ddb.send(new QueryCommand({
    // ... boring stuff
  }));
  return result.Items || [];
};

const getCategory = async (categoryId: string) =&amp;gt; {
  const result = await ddb.send(new GetItemCommand({
    // ... boring stuff
  }));
  return result.Item;
};

async function generateBatchRequests(
  categoryId: string,
  model: string,
  numberOfTips: number
): Promise&amp;lt;BatchRequestInput[]&amp;gt; {
  try {
    // Fetch initial data in parallel
    const [category, topics] = await Promise.all([
      getCategory(categoryId),
      getTopicsForCategory(categoryId),
    ]);

    if (!category?.id.S) {
      throw new Error(`Category not found: ${categoryId}`);
    }

    console.log('Generating batch requests for category', category.id.S);
    console.log('Found topics:', topics.length);

    const batchRequests: BatchRequestInput[] = [];

    // Process topics
    for (const topic of topics) {
      if (!topic.id.S) continue;

      const sections = await getSectionsForTopic(topic.id.S);
      console.log(`Found ${sections.length} sections for topic ${topic.id.S}`);

      // Process sections
      for (const section of sections) {
        if (!section.id.S) continue;

        const units = await getUnitsForSection(section.id.S);
        console.log(`Found ${units.length} units for section ${section.id.S}`);

        // Process units
        for (const unit of units) {
          if (!unit.id.S) continue;

          const event = {
            // ... any custom data used in your prompts
            model: model,
          };

          const customId = uuidv4();
          batchRequests.push({
            custom_id: customId,
            lineItem: {
              custom_id: customId,
              method: 'POST',
              url: '/v1/chat/completions',
              body: {
                model: model,
                messages: [
                  { role: 'system', content: getSystemPrompt(event) },
                  { role: 'user', content: getUserPrompt(event) }
                ],
                response_format: { type: "json_object" },
                temperature: 0.3,
              },
            },
            // ... any other field you may want to correlate later on post processing
          });

          console.log(`Created batch request for unit ${unit.id.S}`);
        }
      }
    }

    console.log(`Total batch requests generated: ${batchRequests.length}`);
    return batchRequests;

  } catch (error) {
    console.error('Error generating batch requests:', error);
    throw error;
  }
}

interface GenerateTipsBatchRequestEvent {
  categoryId: string;
  model: string;
  numberOfTips: number;
}

export const handler = async (event: GenerateTipsBatchRequestEvent) =&amp;gt; {
  try {
    const { categoryId, model, numberOfTips } = event;
    if (!model || !categoryId || !numberOfTips) {
      return {
        statusCode: 400,
        body: JSON.stringify({ error: 'Missing required parameters' }),
      };
    }

    await initOpenAi();

    console.log('Generating batch requests');
    const batchRequestInputs = await generateBatchRequests(categoryId, model, numberOfTips);

    if (batchRequestInputs.length === 0) {
      return {
        statusCode: 404,
        body: JSON.stringify({ error: 'No units found' }),
      };
    }

    const files = await createBatchFile(batchRequestInputs.map(batchRequestInput =&amp;gt; batchRequestInput.lineItem));

    for (const file of files) {
      console.log('Uploading file ', file.name);
      const upload = await openai?.files.create({
        purpose: 'batch',
        file: file
      });
      if (!upload) continue;

      console.log('File uploaded', JSON.stringify(upload, null, 2));

      console.log('Creating batch');
      const requestedBatch = await openai?.batches.create({
        completion_window: '24h',
        endpoint: '/v1/chat/completions',
        input_file_id: upload.id,
        metadata: {
          // ... any metadata you may want to add
        }
      });
      console.log('Batch created', JSON.stringify(requestedBatch, null, 2));

      if (!requestedBatch) continue;

      console.log('Storing batch request', batchRequestInputs.length);
      await storeBatchRequest(batchRequestInputs.map(batchRequestInput =&amp;gt; ({
        customId: batchRequestInput.custom_id,
        body: JSON.stringify(batchRequestInput.lineItem),
        batchId: requestedBatch.id,
        filename: upload?.filename,
        bytes: upload?.bytes,
        status: requestedBatch.status,
        // ... and more fields you may want to correlate later on post processing
      })));

      console.log('Batch request stored');
    }

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Batch request stored',
        batchRequestInputsLength: batchRequestInputs.length,
      }),
    };
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

async function storeBatchRequest(requests: {
  status: string;
  type: string;
  body: string;
  batchId: string | undefined;
  filename: string | undefined;
  bytes: number | undefined;
  customId: string;
  // ... and more properties you want to correlate later on post processing
}[]) {
  try {
    console.log('Starting to store batch requests:', requests.length);

    // Split requests into chunks of 25 (DynamoDB batch write limit)
    const batches = chunk(requests, 25);
    console.log('Split into', batches.length, 'batches');

    for (let i = 0; i &amp;lt; batches.length; i++) {
      const batch = batches[i];
      console.log(`Processing batch ${i + 1} of ${batches.length}`);

      const batchWriteParams: BatchWriteCommandInput = {
        RequestItems: {
          [BATCH_REQUESTS_TABLE]: batch.map(request =&amp;gt; ({
            PutRequest: {
              Item: {
                id: request.customId,
                ...request,
                createdAt: new Date().toISOString(),
                updatedAt: new Date().toISOString()
              }
            }
          }))
        }
      };

      try {
        console.log(`Sending batch write command for batch ${i + 1}`);
        const result = await ddb.send(new BatchWriteCommand(batchWriteParams));

        // Check for unprocessed items
        if (result.UnprocessedItems &amp;amp;&amp;amp; Object.keys(result.UnprocessedItems).length &amp;gt; 0) {
          console.warn('Unprocessed items:', result.UnprocessedItems);
          // Optionally retry unprocessed items
          await retryUnprocessedItems(result.UnprocessedItems);
        }

        console.log(`Successfully processed batch ${i + 1}`);
      } catch (error) {
        console.error(`Error writing batch ${i + 1}:`, error);
        throw error;
      }
    }

    console.log('Successfully stored all batch requests');
  } catch (error) {
    console.error('Error in storeBatchRequest:', error);
    throw error;
  }
}

// Helper function to retry unprocessed items
async function retryUnprocessedItems(unprocessedItems: Record&amp;lt;string, any&amp;gt;) {
  try {
    const retryParams: BatchWriteCommandInput = {
      RequestItems: unprocessedItems
    };
    await ddb.send(new BatchWriteCommand(retryParams));
  } catch (error) {
    console.error('Error retrying unprocessed items:', error);
    throw error;
  }
}

async function createBatchFile(items: BatchRequestLineItem[], maxSizeMB: number = 200): Promise&amp;lt;FileLike[]&amp;gt; {
  const MAX_FILE_SIZE = maxSizeMB * 1024 * 1024; // Convert MB to bytes
  const files: FileLike[] = [];
  let currentItems: BatchRequestLineItem[] = [];
  let currentSize = 0;
  let fileIndex = 0;

  const createBatchFile = async (items: BatchRequestLineItem[], index: number): Promise&amp;lt;FileLike&amp;gt; =&amp;gt; {
    // Convert items to JSONL string
    const jsonlContent = items
      .map(item =&amp;gt; JSON.stringify(item))
      .join('\n');

    // Create file using OpenAI's toFile utility
    return await toFile(
      new Blob([jsonlContent], { type: 'application/jsonl' }),
      `batch_${Date.now()}_${index}.jsonl`
    );
  };

  // Process items and create batches
  for (const item of items) {
    const line = JSON.stringify(item) + '\n';
    const itemSize = Buffer.byteLength(line, 'utf-8');

    // Check if adding this item would exceed the size limit
    if (currentSize + itemSize &amp;gt; MAX_FILE_SIZE) {
      // Create file from current batch
      const file = await createBatchFile(currentItems, fileIndex);
      files.push(file);

      // Reset for next batch
      currentItems = [];
      currentSize = 0;
      fileIndex++;
    }

    // Add item to current batch
    currentItems.push(item);
    currentSize += itemSize;
  }

  // Process remaining items
  if (currentItems.length &amp;gt; 0) {
    const file = await createBatchFile(currentItems, fileIndex);
    files.push(file);
  }

  return files;
}

const getSystemPrompt = (event: GenerateTipsEvent) =&amp;gt; `You are... rest of prompt`

const getUserPrompt = (event: GenerateTipsEvent) =&amp;gt; `Generate ... rest of prompt`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This function ran in 4518.0ms to upload one file of 4Mb with 391 prompts of a total of 1,301,368 tokens (in and out). &lt;br&gt;
The processing of those prompts cost about $14, and it took 18 minutes for the batch to complete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesvclr5rf3bailbtyjqf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesvclr5rf3bailbtyjqf.png" width="800" height="1099"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, the numbers in the title don’t match, but the numbers above are only partial, which matches the generation of tips in the workflow screenshot above. For the sake of simplicity and deduplication, I omitted the processing of quizzes, which follows the same approach.&lt;/p&gt;

&lt;p&gt;Please bear in mind the batch results may be available up to 24h. So it’s something you should consider only if you don’t need an answer right away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning:&lt;/strong&gt; Why do now what we can put off until later?! :)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thanks for reading!&lt;/strong&gt; Got any cool ideas or feedback you want to share? Drop a comment, send me a message or &lt;a href="https://www.linkedin.com/in/fmalaquias/" rel="noopener noreferrer"&gt;follow me&lt;/a&gt; and let’s keep moving things forward!&lt;/p&gt;

</description>
      <category>openai</category>
      <category>chatgpt</category>
      <category>lambda</category>
      <category>costsavings</category>
    </item>
    <item>
      <title>So long DocumentDB, hello MongoDB Atlas</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Sun, 27 Oct 2024 12:11:39 +0000</pubDate>
      <link>https://dev.to/aws-builders/so-long-documentdb-hello-mongodb-atlas-1d14</link>
      <guid>https://dev.to/aws-builders/so-long-documentdb-hello-mongodb-atlas-1d14</guid>
      <description>&lt;h3&gt;
  
  
  Why did we replace DocumentDB with MongoDB Atlas?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo7a1q9tj6u1nlmcf4jn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo7a1q9tj6u1nlmcf4jn.png" alt="CPU IO Waits" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update (August 01, 2025):&lt;/strong&gt; DocumentDB now supports serverless configuration, which is ideal for spikey workloads. See &lt;a href="https://aws.amazon.com/blogs/aws/amazon-documentdb-serverless-is-now-available/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/aws/amazon-documentdb-serverless-is-now-available/&lt;/a&gt; for more information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update (March 16, 2025):&lt;/strong&gt; Since this article was originally published, the AWS DocumentDB team has implemented several significant improvements that address some points raised here, including NVMe support, enhanced memory usage for network I/O, additional compression options, and configurable shard instances. While I believe this article still provides valuable insights, I strongly recommend reviewing the latest AWS DocumentDB and MongoDB Atlas documentation for the most up-to-date information.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Writes &lt;a href="https://pt.wikipedia.org/wiki/IOPS" rel="noopener noreferrer"&gt;IOPS&lt;/a&gt; scaling capabilities (&lt;a href="https://www.mongodb.com/docs/manual/sharding/" rel="noopener noreferrer"&gt;sharding&lt;/a&gt;) and minor perks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This article does not cover application optimization but rather focuses on comparing these two database services.&lt;/p&gt;

&lt;p&gt;Before we start, why did we even use DocumentDB?&lt;/p&gt;

&lt;p&gt;While moving our on-premise workload to AWS, &lt;a href="https://aws.amazon.com/documentdb/" rel="noopener noreferrer"&gt;DocumentDB&lt;/a&gt; seemed like a natural choice to replace our in-house-maintained MongoDB cluster.&lt;/p&gt;

&lt;p&gt;DocumentDB is a robust and reliable database service for MongoDB-based applications and can make your &lt;a href="https://aws.amazon.com/products/storage/lift-and-shift/" rel="noopener noreferrer"&gt;lift and shift&lt;/a&gt; process easier, but there are probably better fits for your needs.&lt;/p&gt;

&lt;p&gt;Why? The reasons vary according to your needs, but they will probably boil down to one or more of the ones described below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did you write your code using MongoDB drivers and expect it to behave like MongoDB? It won’t.
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Amazon DocumentDB is built on top of AWS’s custom Aurora platform, which has historically been used to host relational databases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the first statement on the &lt;a href="https://www.mongodb.com/resources/compare/documentdb-vs-mongodb/architecture" rel="noopener noreferrer"&gt;architectural comparison&lt;/a&gt; at the MongoDB website. Amazon DocumentDB supports MongoDB v4.0 and v5.0, but it does not support all features from those versions or from newer versions (e.g., the latest &lt;a href="https://www.mongodb.com/resources/products/mongodb-version-history" rel="noopener noreferrer"&gt;MongoDB v8.0&lt;/a&gt;). DocumentDB is currently only about &lt;a href="https://www.isdocumentdbreallymongodb.com/" rel="noopener noreferrer"&gt;34% compatible with MongoDB&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Main differences that were relevant for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;no support for index prefix compression&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;no support for network compression&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;poor support of data compression (&lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/doc-compression.html" rel="noopener noreferrer"&gt;requires manual setup&lt;/a&gt; per collection and setup of threshold per document)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;further index type limitations in elastic setup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;built on top of Aurora, hence difficult to keep up with updates and to be able to support the same capabilities as MongoDB&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Low capabilities for scaling write operations
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;While Aurora’s storage layer is distributed, its compute layer is not, limiting scaling options.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the second statement on the &lt;a href="https://www.mongodb.com/resources/compare/documentdb-vs-mongodb/architecture" rel="noopener noreferrer"&gt;architectural comparison&lt;/a&gt; page, and it is true.&lt;/p&gt;

&lt;p&gt;While DocumentDB is great for scaling read-heavy applications (currently, up to 15 instances—see the limits described &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/limits.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;), it provides only a few options for scaling writes, and even so, those features were mainly &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/release-notes.html#release-notes.09-18-2024" rel="noopener noreferrer"&gt;introduced in mid-2024&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That means if your cluster is spending too much time waiting for IO (check your cluster &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/performance-insights-concepts.html" rel="noopener noreferrer"&gt;performance insights&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/best_practices.html" rel="noopener noreferrer"&gt;disk queue depth&lt;/a&gt;) and you need to scale write IOPS/throughput, for example, you can enable the I/O optimized flag, which will reduce costs and use SSD to increase performance. However, I found no clear &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/db-cluster-storage-configs.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; about how many IOPS it translates to, but from tests, it seems like it is using &lt;a href="https://aws.amazon.com/ebs/general-purpose/" rel="noopener noreferrer"&gt;EBS gp3 volumes&lt;/a&gt;, which translates to 3000 IOPS baseline.&lt;/p&gt;

&lt;p&gt;In addition to that, the only remaining options are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scaling vertically:&lt;/strong&gt; very expensive and no clear documentation on how it translates to improved IOPS, as it will result in CPU increase, allowing incoming requests to be processed, but storage will still be a bottleneck for writes)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sharding:&lt;/strong&gt; Sharding in DocumentDB is a &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/release-notes.html#release-notes.09-18-2024" rel="noopener noreferrer"&gt;brand new feature&lt;/a&gt; released in January 2024, and it is still not supported in all regions. In addition, it currently has some serious &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/docdb-using-elastic-clusters.html" rel="noopener noreferrer"&gt;limitations&lt;/a&gt;. For example, last I checked, one could only set 2 nodes per shard, which means very low resilience.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s it. Those are the only options for scaling write operations in DocumentDB at the moment. If financial cost, resilience, and scalability are considered, they will likely not be a fit for a scenario of write-heavy applications that require availability close to 100% and high write IOPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better cloud-native technologies
&lt;/h2&gt;

&lt;p&gt;The last reason could be that you are building a new application and have the flexibility of choosing some other cloud-native technology that better fits your use case without the concern of having to refactor your application’s data layer and introduce breaking changes.&lt;/p&gt;

&lt;p&gt;Document-based DBs offer great flexibility for storing data, but nowadays, several technologies, like DynamoDB, can provide better scalability and throughput with serverless offers. However, that requires a different approach for your application, each with its pitfalls.&lt;/p&gt;

&lt;p&gt;If you are writing a brand new application, think about how your data changes over time, how it is structured, how often it’s going to be inserted, updated, deleted, queried, think about the required availability for your DB cluster, its resilience, costs vs. risk of financial loss with downtimes, etc., and try to choose a technology and sizing that better fits your specific use case. You’ll highly likely end up with something else other than DocumentDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the benefits of migrating from DocumentDB to MongoDB Atlas?
&lt;/h2&gt;

&lt;p&gt;In short, besides vertical scaling (usually a much more expensive solution), MongoDB has better support for sharding, different instance types offering (low-CPU, general, and NVMe), and provisioned IOPS, which offers more fine-grained control over your cluster capabilities and costs.&lt;/p&gt;

&lt;p&gt;The table below shows a brief comparison between both solutions and their current functionality as of the writing of this article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs388cm9ayjmpjiiardxk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs388cm9ayjmpjiiardxk.png" alt="DocumentDB vs MongoDB Atlas brief comparison" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One can also set up an Atlas cluster using &lt;a href="https://www.mongodb.com/docs/atlas/security-private-endpoint/" rel="noopener noreferrer"&gt;private links&lt;/a&gt;, which might improve latency (especially for queries that require &lt;a href="https://www.mongodb.com/docs/manual/reference/command/getMore/" rel="noopener noreferrer"&gt;getMore&lt;/a&gt;), but I haven’t tested this yet.&lt;/p&gt;

&lt;p&gt;In addition, one can write code to have some compute autoscaling in DocumentDB, but it’s not supported out of the box. For more info, see recommendations for DocumentDB scaling &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/db-cluster-manage-performance.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  NVMe instances
&lt;/h2&gt;

&lt;p&gt;At first, I thought NVMe instances would solve all our problems. Don’t let yourself get fooled by it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o7kz4r8bbkj1p1c4y1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o7kz4r8bbkj1p1c4y1j.png" alt="MongoDB Atlas M40 Local NVMe SSD current offering" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, they do provide crazy amounts of IOPS, which may result in excellent throughput, and they cost relatively low in comparison to obtaining way poorer results with something like provisioned IOPS (capped at max. 6000 due to storage block restrictions from AWS). Still, it also comes at a more significant cost: &lt;strong&gt;a significantly long time to recover&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In general, NVMe clusters are very robust and performant, but to achieve such high storage throughput, they work with locally attached ephemeral &lt;a href="https://www.mongodb.com/docs/atlas/manage-clusters/#std-label-nvme-storage" rel="noopener noreferrer"&gt;NVMe&lt;/a&gt; (non-volatile memory express) SSDs. As a consequence, a &lt;a href="https://www.mongodb.com/docs/manual/core/replica-set-sync/#file-copy-based-initial-sync" rel="noopener noreferrer"&gt;file copy based initial sync&lt;/a&gt; will always be used to sync all of the nodes of an NVMe cluster whenever an initial sync is required, and because of that, if you need to scale your cluster up/down, or recover backups, you will experience a very long time to perform these operations, and if you need to scale fast, you will find yourself in a terrible situation.&lt;/p&gt;

&lt;p&gt;My advice? Avoid those as much as you can by optimizing your application and scaling the DB cluster horizontally by adding shards if you need write IOPS, simply scaling the number of instances if you need read IOPS only, or both in the worst case. You might even end up with a cheaper price for similar or even better performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Low-CPU instances
&lt;/h2&gt;

&lt;p&gt;Those instances are great if your application doesn’t require much processing on the DB side, which is a good practice.&lt;/p&gt;

&lt;p&gt;CPU and memory are much more valuable resources on the DB side than in your application container. Scaling application containers and distributing parallel processing is a much cheaper and less time-sensitive operation than scaling DB clusters, especially with all the easy-to-use and great capabilities of Kubernetes, Karpenter in combination with spot instances, and so on.&lt;/p&gt;

&lt;p&gt;By using low CPU instances, you may benefit from better pricing when choosing instances with higher memory available. This is very important for caching and consequently speeds up your queries by reducing the need to load data from disk, which is slow and can quickly degrade the performance of your cluster.&lt;/p&gt;

&lt;p&gt;If you have questions about sizing, I recommend reading the &lt;a href="https://www.mongodb.com/docs/atlas/sizing-tier-selection/" rel="noopener noreferrer"&gt;official MongoDB documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  General instances
&lt;/h2&gt;

&lt;p&gt;Those instances have double the CPU than the low-CPU tier, but they also come at about 20% price increase. So, if you require processing peaks and can afford the price, go for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atlas Console
&lt;/h2&gt;

&lt;p&gt;Atlas console offers great features for executing queries and aggregation pipelines in your databases, as well as intelligent detection of inefficient indexes and much more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9iokpdgem19n1dyhwl0n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9iokpdgem19n1dyhwl0n.png" alt="MongoDB Atlas Collection View" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because of the features offered within the console and the ease of connecting to the cluster through Mongo Shell, we no longer need third-party tool licensing, such as &lt;a href="https://studio3t.com/" rel="noopener noreferrer"&gt;Studio3T&lt;/a&gt;, for example.&lt;/p&gt;

&lt;p&gt;In addition, it offers much more in-depth metrics than DocumentDB for analyzing your cluster, like how much data is being compressed on disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdmhgw8qg4u89jhzvsuh6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdmhgw8qg4u89jhzvsuh6.png" alt="Normalized system CPU metrics of sharded cluster" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might want to ship these metrics to another place like Grafana, though, because if you want to analyze peaks in the past, MongoDB Atlas metrics will be calculated as the average of 1h to save some processing, and therefore, they will not be very useful in that regard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Insights
&lt;/h2&gt;

&lt;p&gt;The main reason we opted to migrate from DocumentDB to MongoDB Atlas was the capability to scale write throughput. Still, I have to confess that the metrics and tools offered by Atlas make developers' lives much easier by providing an excellent overview of overall DB performance, pointing out slow queries that may highly likely be optimized on the application side, consequently making applications faster and more reliable to the final users, and providing opportunities to reduce costs by fine-tuning the DB cluster according to your needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzs8ktwkhrqnehmkyjdm6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzs8ktwkhrqnehmkyjdm6.png" alt="P99 latency of reads and writes operations" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Profiler
&lt;/h2&gt;

&lt;p&gt;The query profiler clusters queries so that you can analyze how the engine processed them in great detail. When you click in one of those clusters below, you will find information about how many keys were examined, how many documents were read, how long the query planner took to process the query, how long it took to read the documents from disk, and much more.&lt;/p&gt;

&lt;p&gt;The coloring also makes it very easy to identify the slowest collections in your DB, which may help to identify strange access patterns and inefficient data structure and/or indexing, among other possible problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1zbeqpcqbbtkv7amabu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1zbeqpcqbbtkv7amabu.png" alt="Slow queries overview" width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Support
&lt;/h2&gt;

&lt;p&gt;I believe nobody can provide better support for some service, tooling, or framework than the source itself. So, if you have a MongoDB-based application, I think MongoDB experts may be able to help you :)&lt;/p&gt;

&lt;p&gt;We had a good experience with tailored support for evaluating and identifying bottlenecks, exchanging solutions, and sizing our cluster. Although AWS also offers good support, from my personal experience, DocumentDB experts will only analyze the health of your cluster itself, but will not dive deep into your needs and make recommendations based on your application implementation.&lt;/p&gt;

&lt;p&gt;As we have an enterprise contract with MongoDB Atlas (no, they don’t sponsor this article by any means, and all the content here expresses my own opinion and experience), we could benefit from an in-depth analysis of our needs before we migrate the data until after go live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawbacks
&lt;/h2&gt;

&lt;p&gt;If you migrate things as they are without identifying issues in your application and solving them beforehand, you might see yourself paying a lot for overprovisioning your cluster, as they are not the cheapest thing around.&lt;/p&gt;

&lt;p&gt;In addition, it might add complexity to your setup and require developers to obtain more in-depth knowledge of DB setup, sharding, and query optimization. Still, I do see this as a benefit. Knowing things work without knowing why is dangerous.&lt;/p&gt;

&lt;p&gt;On the other hand, more complexity and fine-tuning opportunities also pose more risks of messing things up, so you will need to pay more attention to details while setting the DB up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compute Auto-Scaling
&lt;/h2&gt;

&lt;p&gt;As odd as it sounds, I considered adding auto-scaling under the drawbacks session. The reason is that as good as auto-scaling sounds, and as good as it is portrayed in MongoDB Atlas’ documentation, it may cause more harm than good in your applications.&lt;/p&gt;

&lt;p&gt;The reason is that the autoscaling happens on a rolling basis, which is ok. However, it will take nodes down one by one before updating them, which will cause the performance of your cluster to degrade even further because the load is shifted to the other remaining nodes and may lead to a downtime for a longer time than it would in case the autoscaling would be disabled and your application could have stabilized due to caching and other mechanisms. Therefore, if your application needs to handle such peaks without any unavailability, you might need to disable autoscaling and overprovision beforehand, knowing when your application expects peaks, for example.&lt;/p&gt;

&lt;p&gt;If this scenario is not your concern, auto-scaling might be a handy tool for optimizing costs while dealing with extra load when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hatchet
&lt;/h2&gt;

&lt;p&gt;Well, that has nothing to do with MongoDB Atlas itself, but I learned this tool from a MongoDB consultant during one of our sessions and thought it would be helpful to share it here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2108%2F0%2AnePMG1s7yfjbULIc" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2108%2F0%2AnePMG1s7yfjbULIc" alt="Hatchet log summary — example extracted from github repository" width="1054" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/simagix/hatchet" rel="noopener noreferrer"&gt;Hatchet&lt;/a&gt; is a MongoDB JSON log analyzer and viewer implemented by someone from MongoDB that provides great support for query analysis. It also has a text search that makes it quicker to find issues. You just need to export the logs directly from the MongoDB console and import them into Hatchet, which will provide you with a summary of the insights in addition to some details about them.&lt;/p&gt;

&lt;p&gt;Check it out if you ever need to go through MongoDB logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and Costs
&lt;/h2&gt;

&lt;p&gt;Finally, let’s talk about what really matters.&lt;/p&gt;

&lt;p&gt;Before discussing performance and cost comparisons, let’s discuss our use case so we can better understand the problem.&lt;/p&gt;

&lt;p&gt;This specific database serves two backend services. One backend service (let’s call it the Writer application) listens to several Kafka topics, aggregates the data in an optimal way for reads by the other service, and writes it to the DB. It connects to primaries only (primary &lt;a href="https://www.mongodb.com/docs/manual/core/read-preference/" rel="noopener noreferrer"&gt;read preference&lt;/a&gt;) and is write-heavy with few parallel connections to the DB.&lt;/p&gt;

&lt;p&gt;In this Writer application, we want to keep &lt;a href="https://docs.confluent.io/cloud/current/_glossary.html#term-consumer-lag" rel="noopener noreferrer"&gt;consumers lag&lt;/a&gt; always close to 0 in order to provide real-time, up-to-date data to the other application (let’s call it the Reader application). If we have lags in this application, it translates to outdated data in the Reader application, which should not happen (or at least it should be as close to real-time as possible).&lt;/p&gt;

&lt;p&gt;The Reader application will connect to secondaries preferably (secondaryPreferred read preference) and is a read-only application that will perform thousands of queries per second and provide some output to other applications. The Reader application is read-heavy, and latency is also very critical, in addition to high availability. This application must run 24 hours a day, 365 days a year, with an overall average latency per processed request under 100ms, which translates ideally to something less than 10ms per DB query on average.&lt;/p&gt;

&lt;p&gt;Scaling read operations in DocumentDB is not a problem and is not very expensive. One scales the number of replicas, distributes the load among them, and is done.&lt;/p&gt;

&lt;p&gt;Scaling write operations in DocumentDB is, however, the challenge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7c543en8kswd5x65ramz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7c543en8kswd5x65ramz.png" alt="Kafka consumer lag caused by CPU IO wait leading to outdated data in DB reader nodes" width="800" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you see in the example above, there were times when peaks of updates in some of those Kafka topics took a long time to process the data and store it in the DB. This was mainly because of CPU waits in the single primary node in our DocumentDB cluster, which already had CPU overprovisioned as a failed attempt to scale IOPS (CPU won’t scale IOPS further the storage capacity).&lt;/p&gt;

&lt;p&gt;That is how sharding solves the problem. By distributing the data across multiple primary nodes, each in its shard, we can scale write operations horizontally, similarly to how we scale read operations by increasing the number of nodes. So, let’s say one primary can handle 3000 IOPS. By distributing the data over three shards, we increase the capacity three times to 9000 IOPS if your data is distributed evenly.&lt;/p&gt;

&lt;p&gt;Unfortunately, DocumentDB has very low support for sharding (&lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/elastic-how-it-works.html" rel="noopener noreferrer"&gt;elastic cluster&lt;/a&gt;), offering only two nodes per shard, which means low resilience for critical workloads.&lt;/p&gt;

&lt;p&gt;Be careful when dealing with shards, though. Sharding collections may make them way less efficient. So you’ll need to dig into optimizations, access patterns, index efficiency, and many more aspects before deciding to shard your collections. Also, be extra careful when choosing your shard keys to avoid &lt;a href="https://www.mongodb.com/docs/manual/core/sharding-troubleshooting-shard-keys/#uneven-load-distribution" rel="noopener noreferrer"&gt;hot shards&lt;/a&gt; and query inefficiency.&lt;/p&gt;

&lt;p&gt;So, what does it mean in terms of performance and costs?&lt;/p&gt;

&lt;p&gt;In DocumentDB, we operated a &lt;a href="https://aws.amazon.com/about-aws/whats-new/2023/11/amazon-documentdb-i-o-optimized/" rel="noopener noreferrer"&gt;IO optimized&lt;/a&gt; db.r6g.8xlarge cluster, which costed us about EUR11k/month. In MongoDB Atlas, we used a M40 cluster with 4 shards and 3 nodes each for initial tests and comparison, which would cost us about half of the price — EUR4.7k/month.&lt;/p&gt;

&lt;p&gt;The best thing is that in Atlas, you are not restricted to only two nodes per shard, which significantly helps resilience and read load distribution.&lt;/p&gt;

&lt;p&gt;In our tests, we used one specific collection that has a high frequency of updates and is very large in size, which was a very good candidate for sharding. We basically reset the offset of the consumer writing to this collection and waited for it to process all messages in both MongoDB Atlas and DocumentDB clusters and obtained the following results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy824i3wolgf6vue1oiqw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy824i3wolgf6vue1oiqw.png" alt="DocumentDB updated documents per minute count" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnpas7bou3uonslxkeuj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnpas7bou3uonslxkeuj.png" alt="MongoDB Atlas M40, 4 shards, documents updated per second count" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you convert DocumentDB metrics to updated documents per second, the throughput in MongoDB Atlas sharded cluster is about 5 times higher than in DocumentDB with no shards. Not to mention, the CPU was blocked most of the time waiting for IO in DocumentDB, which would make it very slow for processing other data, and as a consequence, leading to multiple outdated collections and slowness in processing all writes in the single primary node.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2d3ugm86qyul71zcieo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2d3ugm86qyul71zcieo.png" width="800" height="945"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The difference can also be seen at client side, in the Writer application, by looking at its consumer Kafka lag as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3tut2xby33d05cup8v0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3tut2xby33d05cup8v0f.png" alt="Kafka lag while resetting offsets with DocumentDB cluster with single primary node" width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz4l4he8qcgksysob7i45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz4l4he8qcgksysob7i45.png" alt="Kafka lag while resetting offsets with MongoDB Atlas M40 4 Shards" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While DocumentDB processed all messages in about two hours, the sharded cluster in MongoDB Atlas took about 20 minutes.&lt;/p&gt;

&lt;p&gt;Note that the tests were performed at different dates and timestamps, so the lag won’t match precisely the 5 to 7 times higher throughput, as the test with MongoDB Atlas was performed earlier at a point in time when the Kafka topic had fewer messages than it had when tested against DocumentDB. Therefore, in this case, the primary metric for comparison is the updated documents per second, but you can still grasp what it means in terms of impact to keep data up to date by looking into the Kafka lag metric.&lt;/p&gt;

&lt;p&gt;In summary, we were able to achieve about five times better throughput by spending half the money. In reality, our setup is slightly different at the moment, and we end up paying about the same price we used to for DocumentDB, but that has to do with current autoscaling capabilities and shifting load during the scaling process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provisioned IOPS
&lt;/h2&gt;

&lt;p&gt;Sharding is great for scaling write operations. However, it is not something you can use to quickly scale your IOPS when your system is already under heavy load, as it requires balancing documents between the shards. This process takes both time and resources, and it is usually scheduled to run during known periods of low traffic in your DB so as not to affect the performance of your application. &lt;/p&gt;

&lt;p&gt;MongoDB Atlas offers the possibility of provisioning IOPS on demand as a tool for scaling IOPS from 3000 to 6000 per shard. This allows doubling the IOPS capacity of the complete cluster in a matter of minutes to enable more read/write capacity without the need to create new shards and wait for the cluster to be balanced, for example. &lt;/p&gt;

&lt;p&gt;One could use provisioned IOPS as a temporary solution for a short period, postponing the creation of new shards, as provisioned IOPS tends to be relatively expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It is not the technology that is good or bad; perhaps it’s being misused, or it’s not the best fit for your needs. No size fits all.&lt;/p&gt;

&lt;p&gt;Don't change anything if you have no problems (cost, maintenance, performance, availability, etc.). Spend your time somewhere else.&lt;/p&gt;

&lt;p&gt;If you do have some real problem to solve, get to know your data and your write and read patterns. Dig into query and index optimizations before you even think about any migration. Invest time in understanding what is really happening in your application that is causing latencies. Do not simply throw more money at cloud providers to scale things up indefinitely, postponing the unavoidable review of your own code and choices.&lt;/p&gt;

&lt;p&gt;If you are writing a new application, look for database solutions that fit your needs and see if there is a better fit. It could be an SQL database, a serverless database, or both. Perhaps you expect a lot of changes in your data structure and want to opt for document-based DBs, perhaps DynamoDB or even MongoDB Atlas.&lt;/p&gt;

&lt;p&gt;Thankfully, the number of choices nowadays is vast, and some technologies will better suit your use case than others.&lt;/p&gt;

&lt;p&gt;If you need to scale writes, consider sharding or some of the newer serverless options with provisioned IOPS and alikes (be careful with provisioned IOPS, though, as they tend to be very expensive).&lt;/p&gt;

&lt;p&gt;And, very importantly, make decisions backed by data and facts. Perform benchmark tests and try different technologies and scenarios. How is the monitoring? Check their recovery and scaling capabilities. Know their support and be well prepared to avoid unexpected costs.&lt;/p&gt;

&lt;p&gt;Good luck with your decisions!&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>aws</category>
      <category>docdb</category>
      <category>database</category>
    </item>
    <item>
      <title>Accelerating App Development with AppSync Gen2 &amp; Generative AI</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Mon, 08 Apr 2024 18:20:39 +0000</pubDate>
      <link>https://dev.to/aws-builders/accelerating-app-development-with-appsync-gen2-generative-ai-3l21</link>
      <guid>https://dev.to/aws-builders/accelerating-app-development-with-appsync-gen2-generative-ai-3l21</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In this insightful AWS meetup hosted by Idealo in their Berlin office, Felipe Malaquias delves into the transformative power of AWS Amplify Gen2 and generative AI in expediting app development. AWS Amplify Gen2 offers an array of libraries and services and empowers frontend developers to create full-stack applications with ease. With it, creating robust applications becomes seamless, requiring minimal infrastructure knowledge. He'll also explore the burgeoning field of generative AI, including large language models, showcasing their potential to revolutionize innovation and time to market.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/uIh3cI3FgTg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awscommunity</category>
      <category>awsmeetup</category>
      <category>ai</category>
    </item>
    <item>
      <title>Secure Proxy Server in AWS</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Tue, 26 Mar 2024 18:40:32 +0000</pubDate>
      <link>https://dev.to/aws-builders/secure-proxy-server-in-aws-3cfc</link>
      <guid>https://dev.to/aws-builders/secure-proxy-server-in-aws-3cfc</guid>
      <description>&lt;h2&gt;
  
  
  Securely access third-party content with whitelisted IP from wherever you are.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2BV7d5qc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/9204/0%2A98Yr1hQnNRYPnDqr" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2BV7d5qc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/9204/0%2A98Yr1hQnNRYPnDqr" alt="Photo by [Sander Weeteling](https://unsplash.com/@sanderweeteling?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to jump directly to the solution using CDK, go &lt;a href="https://github.com/malaquf/aws-cdk-proxy-server"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Proxy servers are used for several purposes, and in general they provide a gateway between users and the destination they want to access.&lt;/p&gt;

&lt;p&gt;In this example, we will tackle the scenario where you want to access third-party content protected by a firewall that can only be accessed from specific white-listed IPs wherever you are.&lt;/p&gt;

&lt;p&gt;Well, I’m sure there are a couple of ways to solve it, but here I’ll describe a solution that doesn’t depend on increasing too much costs and complexity in your infrastructure, like dealing with &lt;a href="https://aws.amazon.com/vpn/"&gt;VPN clients&lt;/a&gt; or &lt;a href="https://aws.amazon.com/directconnect/"&gt;Direct Connect&lt;/a&gt; links, while not compromising security.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eqDwJiHQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2AIUvXKo8sELwODlP_5ytZaA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eqDwJiHQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2AIUvXKo8sELwODlP_5ytZaA.png" alt="Tunneling to EC2 on a private subnet with SSM" width="741" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although in general &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html"&gt;NAT Gateways&lt;/a&gt; should be avoided, as they might incur additional &lt;a href="https://www.cloudzero.com/blog/reduce-nat-gateway-costs"&gt;unnecessary costs&lt;/a&gt; when outbound connectivity from private subnets could be solved differently (e.g., by using &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html"&gt;VPC endpoints&lt;/a&gt; or &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html"&gt;IPv6 egress-only internet gateways&lt;/a&gt;), in this case, I opted to configure my &lt;a href="https://repost.aws/knowledge-center/nat-gateway-vpc-private-subnet"&gt;VPC with a private subnet with NAT Gateway&lt;/a&gt; so I would have enough flexibility at my side to control how I want to proxy the requests to the third party service while still maintaining a fixed small list of public IPs which would be whitelisted at the third part service (e.g.: 3 &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html"&gt;elastic IPs&lt;/a&gt; from NAT Gateway, one for each &lt;a href="https://aws.amazon.com/about-aws/global-infrastructure/regions_az/"&gt;AZ&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For the proxy server, I chose to use &lt;a href="https://aws.amazon.com/ec2/"&gt;EC2&lt;/a&gt; with &lt;a href="https://www.squid-cache.org/"&gt;Squid Cache&lt;/a&gt; as, in this case, I wanted to keep things simple while I also didn’t need &lt;a href="https://uptime.is/"&gt;4 9s availability&lt;/a&gt; for this server, and if I ever need it, I can restart it or quickly spin up a new one. Of course, if you want to go cheaper, you might also consider &lt;a href="https://aws.amazon.com/de/ec2/spot/"&gt;spot instances&lt;/a&gt; or set &lt;a href="https://repost.aws/knowledge-center/start-stop-lambda-eventbridge"&gt;functions for starting and stopping the instance&lt;/a&gt; when you need it, or even do it manually if it’s a once-in-a-while usage.&lt;/p&gt;

&lt;p&gt;A couple of important things about the EC2 setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Do not assign a key pair for login (it is not needed; generally, having long-living keys lying around is a security risk).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Assign the EC2 instance to a private subnet to reduce exposure to the public internet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use a security group with no INGRESS rules (we will use &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-create-vpc.html"&gt;SSM VPC endpoints&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-methods.html"&gt;instance connect&lt;/a&gt; to access it instead).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure that EGRESS TCP is enabled for all (or restrict it to ephemeral ports used to communicate with AWS services and the services you want to allow your proxy access).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Make sure you assign the ‘AmazonSSMRoleForInstancesQuickSetup’ IAM instance profile (or a custom one with the same permissions) to it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use Amazon Linux 2 or newer AMI (&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-prerequisites.html#eic-prereqs-amis"&gt;check which AMIs support instance connect&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a ‘Name’ tag with a value of ‘proxy-server’, for example, so you can easily automate the tunnel creation later with a script.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the following user data for installing and starting squid-cache, instance connect and SSM agent:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

yum update -y -q

sudo yum install ec2-instance-connect
sudo systemctl enable amazon-ssm-agent
sudo systemctl start amazon-ssm-agent

sudo yum -y install squid

sudo service squid restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To access the EC2 instance, we will use instance connect. As the EC2 is in a private subnet, we need to create the following VPC Endpoints to be able to access it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ssm.&lt;em&gt;region&lt;/em&gt;.amazonaws.com&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ssmmessages.&lt;em&gt;region&lt;/em&gt;.amazonaws.com&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ec2messages.&lt;em&gt;region&lt;/em&gt;.amazonaws.com&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, you need a role you will assume for creating a tunnel and port forwarding to your proxy server, through a temporary ssh session started by SSM. This role must have the following permissions:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "ssm:StartSession",
        "ec2-instance-connect:SendSSHPublicKey"
      ],
      "Resource": [
        "arn:aws:ec2:*:*:instance/*"
      ],
      "Condition": {
        "StringEquals": { "aws:ResourceTag/Name": "proxy-server" }
      }
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "ssm:StartSession"
      ],
      "Resource": [
        "arn:aws:ssm:*:*:document/AWS-StartSSHSession"
      ]
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "ssm:TerminateSession",
        "ssm:ResumeSession"
      ],
      "Resource": ["arn:aws:ssm:*:*:session/$${aws:username}-*"]
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances"
      ],
      "Resource": "*"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The statements above give permissions for describing all EC2 instances in your account, sending SSH public keys, and starting and terminating sessions on EC2 instances with the ‘proxy-server’ name.&lt;/p&gt;

&lt;p&gt;On your computer, to start the session and create the tunnel, you need to install and configure the aws-cli and the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html"&gt;session manager plugin for aws-cli&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, you can create the tunnel with the following script:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FORWARDED_PORT=3128
AWS_REGION=&amp;lt;&amp;lt;your AWS region&amp;gt;&amp;gt;

ec2_instance_id=$(aws ec2 describe-instances \
  --filters Name=tag:Name,Values=proxy-server Name=instance-state-name,Values=running \
  --output text --query 'Reservations[*].Instances[*].InstanceId')

ec2_az=$(aws ec2 describe-instances \
                --filters Name=tag:Name,Values=proxy-server Name=instance-state-name,Values=running \
                --output text --query 'Reservations[*].Instances[*].Placement.AvailabilityZone')


echo "Generating temporary keys"

TMP=$(mktemp -u key.XXXXXX)".pem"

ssh-keygen -t rsa -f "$TMP" -N "" -q -m PEM

aws ec2-instance-connect send-ssh-public-key \
  --region ${AWS_REGION} \
  --instance-id ${ec2_instance_id} \
  --availability-zone ${ec2_az} \
  --instance-os-user ec2-user \
  --ssh-public-key "file://$TMP.pub"

ssh -i $TMP \
      -Nf -M \
      -L ${FORWARDED_PORT}:localhost:${FORWARDED_PORT} \
      -o "UserKnownHostsFile=/dev/null" \
      -o "StrictHostKeyChecking=no" \
      -o IdentitiesOnly=yes \
      -o ProxyCommand="aws ssm start-session --target %h --document AWS-StartSSHSession --parameters portNumber=%p --region=eu-central-1" \
      ec2-user@${ec2_instance_id}

rm $TMP "$TMP.pub"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Before running the script above, ensure your aws-cli sso is properly configured to access the account you deployed your proxy server to.&lt;/p&gt;

&lt;p&gt;The script above will create a temporary key and upload it to the ec2 instance, enabling you temporary access to this instance with this key. The key is automatically removed after 60 seconds, and if you haven’t accessed your ec2 instance with that key during that period, you’d need to create and send a new one. After the key is sent, a port forwarding proxy is created on port 3128. Finally, you can access the third-party content through your proxy by using localhost:3128, as in the curl example below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -x "127.0.0.1:3128" "http://httpbin.org/ip"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To destroy the tunnel, you may execute the following command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lsof -P | grep ':'${FORWARDED_PORT} | awk '{print $2}' | xargs kill -9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That’s it. Consider fine-tuning /etc/squid/squid.conf to secure it even further against misuse.&lt;/p&gt;

&lt;p&gt;Example with CDK is available on &lt;a href="https://github.com/malaquf/aws-cdk-proxy-server"&gt;github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cdk</category>
      <category>squid</category>
      <category>proxy</category>
    </item>
    <item>
      <title>Simple and Cost-Effective Testing Using Functions</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Mon, 25 Mar 2024 10:35:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/simple-and-cost-effective-testing-using-functions-42dm</link>
      <guid>https://dev.to/aws-builders/simple-and-cost-effective-testing-using-functions-42dm</guid>
      <description>&lt;h2&gt;
  
  
  Don’t limit yourself to pipeline tests
&lt;/h2&gt;

&lt;p&gt;While tests in your pipeline are a must, you should not have the false idea that things are fine because your pipeline is green, and here are a couple of reasons why:&lt;/p&gt;

&lt;h3&gt;
  
  
  Complexity in Distributed Systems
&lt;/h3&gt;

&lt;p&gt;You’re not alone. Nowadays, a service is rarely an isolated system with no connections to other services, be it databases, Kafka clusters, other services of your team, other team’s services, third-party services, you name it.&lt;/p&gt;

&lt;p&gt;Imagine the following system:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcl4a5nzutl54o9gabpy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcl4a5nzutl54o9gabpy.png" alt="Simple system with multiple integrations" width="331" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You deploy a shiny new feature for your web login in the web frontend service, test it locally, create unit and integration tests, deploy, and make sure it works in production.&lt;/p&gt;

&lt;p&gt;After some time, you or someone else (of course, always someone else 😛), wanted to provide an MFA feature for mobile apps, and, therefore, modified the account service to provide some additional context to the apps and ended up breaking the login for the web frontend. Let’s say neither account service nor mobile app is your team's responsibility. How long would it take for you to know this feature is broken? Of course, you have metrics and alarms in place, but let’s make it less obvious. Instead of breaking the feature completely, you only break it for a small subset of users, for example. Depending on your thresholds for your alarms, evaluation period, points to alarm, etc., you may take a very long time to detect it (and by very long, I consider it already 5 minutes or above). Or worse, if you don’t have alarms and metrics in place (shame on you), it could detect it only during your next build or after a couple of customer complaints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fail Fast Fix Faster
&lt;/h3&gt;

&lt;p&gt;As mentioned in the previous section, it may take time to detect a failure, and as a consequence, even more time to detect the root cause, as you may not be able to find out so quickly when it started to happen (e.g., short log retention time, missing metrics, etc.). If you are constantly testing your system, you know exactly when something stopped working, making it easier to find the subset of changes during that timeframe that could have led to the issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hidden Intermittent Failures
&lt;/h3&gt;

&lt;p&gt;There are a few things that irritate me more than green peas and the act of restarting a failed build and if it works, proceeding as if everything is fine and it was just a ‘glitch’.&lt;/p&gt;

&lt;p&gt;There is no such thing as a ‘glitch’ in mathematics and, therefore, computer science. Behind everything, there is a reason, and you should always know the reason so you do not get caught off guard in the near future. If an issue can happen, it will happen. Did you get it? Are you sure?&lt;/p&gt;

&lt;p&gt;I’ve seen teams run buggy software for days, months, and even years without fixing intermittent failures because they seemed just randomness, and no one could explain the reason because the frequency was relatively low that no one bothered to check the root cause, and at some point in time, this issue comes and bites you, because if you don’t know the reason, you might make the same mistake again, in another scenario, service, or system that will lead to a higher impact on your business.&lt;/p&gt;

&lt;p&gt;Chasing the reason for things to happen should be the number one goal of software engineers because only then can we learn and improve.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, What’s My Suggestion?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Continuously Test Your Applications
&lt;/h3&gt;

&lt;p&gt;By continuously, I really mean continuously, and not only during deployments. Test it at a one-minute frequency, for example, so you have enough resolution to know when things started to go bad and can also know how frequently an issue occurs. Does it always occur? Every x requests? Only during the night quiet period? All these questions can help you find the root cause faster. Also, make sure those tests alarm you in case they are not working properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Possible Solution with Functions
&lt;/h2&gt;

&lt;p&gt;There are a couple of companies out there that offer continuous testing services, such as &lt;a href="https://www.uptrends.com/" rel="noopener noreferrer"&gt;Uptrends&lt;/a&gt;. However, if you’re looking to run some continuous integration tests, I believe you could have a much more cost-effective, simpler, and more useful solution if you build it on your own using &lt;a href="https://www.postman.com/" rel="noopener noreferrer"&gt;Postman&lt;/a&gt; as a basis.&lt;/p&gt;

&lt;p&gt;Postman is a great tool that has been on the market for a very long time. It is very reliable, has very good features for end users, and has enough flexibility to adapt to your needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  More Useful
&lt;/h3&gt;

&lt;p&gt;I realize occasionally that most developers are not very familiar with their APIs. By that, I mean that they often don’t have a shared collection of API calls prepared for running on demand at each stage if needed, for example.&lt;/p&gt;

&lt;p&gt;Postman allows you to share collections of HTTP, GraphQL, gRPC, Websocket, Socket.IO, and MQTT requests and organize them into multiple environments, each with its variables (e.g., hostname, secrets, user names, etc.).&lt;/p&gt;

&lt;p&gt;By sharing these collections with the team, everyone can quickly understand your APIs by calling them whenever needed, at any stage, for example, and, with this, integrate them into their own systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simpler
&lt;/h3&gt;

&lt;p&gt;Before implementing the solution mentioned in this article, I encountered integration test suites written in Java. Therefore, they had their own projects configured with Maven and had a lot of verbose and redundant code for performing and verifying HTTP calls. These projects were checked out during the build and executed for each stage. The execution also needed some spring boot bootstrap time, making the pipeline slower.&lt;/p&gt;

&lt;p&gt;By using Postman, creating new test cases is much quicker and simpler, as it can be created in a user-friendly UI by inserting the address, adding variables as you need for each environment, adding very straightforward individual assertions per test case, and running it with a click of a button for verifying it. See some examples &lt;a href="https://learning.postman.com/docs/writing-scripts/script-references/test-examples/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F534tvs6o1u90juvmfhxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F534tvs6o1u90juvmfhxz.png" alt="Example of Postman collection with HTTP request test for 200 status code" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Effective
&lt;/h3&gt;

&lt;p&gt;You can use Postman for free with some limitations if you like (you can share your collections with up to 3 people), and this would be enough to implement the solution I’ll describe here. However, if you want to share the collections with your team, it’s good to look at their &lt;a href="https://www.postman.com/pricing/" rel="noopener noreferrer"&gt;plans and pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also, by building your infrastructure to run it, you may even be able to run these tests almost for free! The idea behind this infrastructure is to run the tests using functions through a Postman runner to run test collections exported from Postman. Lambda functions are a &lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;very affordable&lt;/a&gt; way of executing code for a short period of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5mry2melmqd4f3fx94q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5mry2melmqd4f3fx94q.png" alt="Continuous Smoke Testing Infrastructure" width="541" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see in the above diagram, &lt;a href="https://aws.amazon.com/eventbridge/" rel="noopener noreferrer"&gt;EventBridge&lt;/a&gt; schedules a &lt;a href="https://aws.amazon.com/pm/lambda/" rel="noopener noreferrer"&gt;lambda function&lt;/a&gt; to be executed periodically. This lambda function retrieves the assets exported from Postman (test collection, environment, and global variables), injects secrets from the secrets manager, executes the tests using the &lt;a href="https://www.npmjs.com/package/newman" rel="noopener noreferrer"&gt;Newman&lt;/a&gt; npm package, and, in case of failures, updates metrics in CloudWatch and stores test results in the S3 bucket. An alarm is triggered if the metrics exceed a threshold (in this case, a count of 1).&lt;/p&gt;

&lt;p&gt;The complete solution with &lt;a href="https://aws.amazon.com/serverless/sam/" rel="noopener noreferrer"&gt;SAM&lt;/a&gt; is available &lt;a href="https://github.com/malaquf/postman-api-testing-sam" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The infrastructure is defined in the &lt;a href="https://github.com/malaquf/postman-api-testing-sam/blob/main/template.yaml" rel="noopener noreferrer"&gt;template.yaml&lt;/a&gt; file, and the lambda function handler with all testing logic is defined in &lt;a href="https://github.com/malaquf/postman-api-testing-sam/blob/main/src/handlers/api-testing-handler.ts" rel="noopener noreferrer"&gt;api-testing-handler.ts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This infrastructure can be reused for any Postman testing (HTTP, REST APIs, etc.). An example of an exported Postman collection is available &lt;a href="https://github.com/malaquf/postman-test-example" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Please notice that these files were not created manually but &lt;a href="https://learning.postman.com/docs/getting-started/importing-and-exporting/exporting-data/" rel="noopener noreferrer"&gt;exported&lt;/a&gt; from the UI. All these files must be placed inside the S3 bucket generated by the infrastructure in the folder defined by the &lt;a href="https://github.com/malaquf/postman-api-testing-sam/blob/aabcd2024ee3524ed76d0c7da05bfeea75838a9a/template.yaml#L9" rel="noopener noreferrer"&gt;*TestName&lt;/a&gt; *parameter input during the infrastructure deployment (in this case, ‘MyService’ by default).&lt;/p&gt;

&lt;p&gt;Also, notice that the &lt;a href="https://github.com/malaquf/postman-api-testing-sam/blob/aabcd2024ee3524ed76d0c7da05bfeea75838a9a/template.yaml#L12" rel="noopener noreferrer"&gt;*SecretId&lt;/a&gt; *secret must exist in order for the lambda function to inject any secret needed by the test collection.&lt;/p&gt;

&lt;p&gt;Have fun playing around with it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>testing</category>
      <category>lambda</category>
      <category>postman</category>
    </item>
    <item>
      <title>A Ride Through Optimising Legacy Spring Boot Services For High Throughput</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Sun, 24 Mar 2024 08:40:22 +0000</pubDate>
      <link>https://dev.to/aws-builders/a-ride-through-optimising-legacy-spring-boot-services-for-high-throughput-477n</link>
      <guid>https://dev.to/aws-builders/a-ride-through-optimising-legacy-spring-boot-services-for-high-throughput-477n</guid>
      <description>&lt;p&gt;Oops! Did I fix it or screw it up for real?&lt;/p&gt;

&lt;p&gt;Even though we could easily scale this system up vertically and/or horizontally as desired, and the load tested was 20x the expected peak, the rate of failed responses on our load tests before my quest was about 8%.&lt;/p&gt;

&lt;p&gt;8% for me is a lot!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;8% of €1.000.000.000.000,00 is a lot of money for me (maybe not for Elon Musk).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;8% of the world’s population is a good number of people&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;8% of a gold bar — I’d love to own it!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;8% of Charlie Sheen’s ex-girlfriends, damn… That must be tough to handle!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why is that? Because of previous alarms and metrics I’ve set along the way, I knew something was off, and our throughput was suboptimal, even considering the fairly small number of pods we were running for these services, even if we are talking about outdated libs/technology. And worst… we are only talking about a few hundred requests per second—it should just work, and at a higher scale!&lt;/p&gt;

&lt;p&gt;As you will see at the end of this article, performing such load tests in your services can reveal a lot of real issues hidden in your architecture, code, and/or configuration. The smell that “something is off” here indicated that something was indeed off, also for regular usage of those services. Chasing the root cause of problems is always worth it — never ignore errors, considering it’s a “hiccup”. There’s no such thing as a “hiccup” in software. The least that can happen is that you learn more about the software you wrote, the frameworks you use, and the infrastructure that hosts it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;As there are so many variables in software development (pun intended), I think context is important in this case, and we will limit talking about optimizations on the following pretty common legacy tech stack (concepts apply to others as well — yes, including the latest shit):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Springboot 2.3 + Thymeleaf&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MongoDB&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Java&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Architectural changes are not the focus of this article, so I assume some basic understanding of resilient architectures and I won’t write about it besides giving a few notes below on what is expected you are aware of when talking about highly available systems (but again, not limited to):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The network is protected (e.g., divided into &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-example-private-subnets-nat.html"&gt;public and private&lt;/a&gt; or equivalent subnets)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The network is resilient (e.g.: redundant subnets are distributed across different &lt;a href="https://aws.amazon.com/about-aws/global-infrastructure/regions_az/"&gt;availability zones&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use clusters and multiple nodes in distributed locations when applicable (e.g. &lt;a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Clusters.html"&gt;Redis cache clusters&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/db-cluster-create.html"&gt;db clusters&lt;/a&gt;, service instances deployed in multiple availability zones, and so on)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;a href="https://aws.amazon.com/elasticloadbalancing/"&gt;load balancers&lt;/a&gt; and distribute load accordingly to your redundant spring boot services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-auto-scaling.html"&gt;autoscaling&lt;/a&gt; in place based on common metrics (e.g.: CPU, memory, latency)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add cache and edge servers to avoid unnecessary service load when possible (e.g.: &lt;a href="https://aws.amazon.com/cloudfront/"&gt;Cloudfront&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a firewall and other mechanisms for protecting your endpoints against malicious traffic and bots before it hits your workload and consume those precious worker threads (e.g.: &lt;a href="https://aws.amazon.com/waf/"&gt;WAF&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Health checks are setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The minimum number of desired instances/pods is set according to your normal load&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For simplification purposes, I’m reducing the context of this article (and therefore the diagram below) to study only the SpringBoot fine-tuning part of it, in a system similar to the following one:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JkG9Gu2z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2A3LVwF57YK51cQAxmMs-PWg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JkG9Gu2z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2A3LVwF57YK51cQAxmMs-PWg.png" alt="Simplified System Diagram" width="641" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First things first
&lt;/h2&gt;

&lt;p&gt;As mentioned, the progress I’m describing here was only possible due to measurements and monitoring introduced before the changes. How can you improve something if you don’t know where you are and have no idea where you want to go? Set the f*cking monitoring and alarms up before you proceed with implementing that useless feature that won’t work properly anyway if you don’t build it right and monitor it.&lt;/p&gt;

&lt;p&gt;A few indicators you may want to monitor in advance (at least, but not limited to):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SpringBoot Service:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;api response codes (5xx and 4xx)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;latency per endpoint&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;requests per second per endpoint&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tomcat metrics (servlet errors, connections, current threads)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CPU&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;memory&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DB Cluster:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;top queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;replication latency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;read/write latency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;slow queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;document locks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;system locks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CPU&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;current sessions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For SpringBoot, this is easily measurable by enabling management endpoints and collecting the metrics using Prometheus and shipping metrics to Grafana or Cloudwatch, for example. After the metrics are shipped, set alarms on reasonable thresholds.&lt;/p&gt;

&lt;p&gt;For the database, it depends on the technology, and you should monitor it at both the client (spring boot db metrics) and server sides. Monitoring on the client side is important to see if any proxy or firewall is blocking any of your commands from time to time. Believe me, these connection drops may happen even if you test it and it seems to work just fine, in case something is not properly configured and you want to catch it! For example, a misconfiguration of outbound traffic on the DB port on your proxy sidecar may lead to dirty HTTP connections at the spring boot side that were already closed on the server side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alright, let’s crash it.
&lt;/h2&gt;

&lt;p&gt;It’s time to set your load test based on your most crucial processes that you know will be under high pressure during peak periods (or maybe that’s already your normal case, and that’s what we should aim for anyway… maximum efficiency with the least amount of resources possible).&lt;/p&gt;

&lt;p&gt;In this case, we chose to use this &lt;a href="https://aws.amazon.com/solutions/implementations/distributed-load-testing-on-aws/"&gt;solution from AWS&lt;/a&gt; for load testing just because the setup is very simple and we already had compatible JMeter scripts ready for use, but I’d rather suggest using &lt;a href="https://docs.locust.io/en/stable/running-cloud-integration.html"&gt;distributed Locust&lt;/a&gt; instead for better reporting and flexibility.&lt;/p&gt;

&lt;p&gt;In our case, we started simulating load with 5 instances and 50 threads each, with a ramp-up period of 5 minutes. This simulates something like 250 clients accessing the system at the same time. Well, this is way above the normal load we have on those particular services anyway, but we should know the limits of our services… in this case, it was pretty much low, and we reached it quite fast — shame on us!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Euvuf75j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2736/1%2AizNGLd5ANEUW83MQPjYm8g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Euvuf75j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2736/1%2AizNGLd5ANEUW83MQPjYm8g.png" alt="" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IxEvNo1q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5284/1%2Al6-VV5uXNMgY1znSz3XJbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IxEvNo1q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5284/1%2Al6-VV5uXNMgY1znSz3XJbw.png" alt="" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SLVcV8Ut--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2020/1%2ATAJMZg_ceDadeoPJCO4XmQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SLVcV8Ut--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2020/1%2ATAJMZg_ceDadeoPJCO4XmQ.png" alt="" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those metrics above were extracted from our API gateways' &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/implementing-logging-monitoring-cloudwatch/cloudwatch-dashboards-visualizations.html"&gt;automatically generated Cloudwatch dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There, you can see a couple of things:&lt;/p&gt;

&lt;p&gt;1- The load test starts around 15:55 and ends around 16:10&lt;br&gt;
2- The load is only applied to one of the services (see “count” metric)&lt;br&gt;
3- The load applied to one upstream service caused latency in three services to increase (the service the load was applied to + 2 downstream services)&lt;br&gt;
4- A high rate of requests failed with 500s on the service we applied the load to&lt;/p&gt;

&lt;p&gt;Therefore, we can conclude that the increased request rate caused bottlenecks in downstream services, which caused latency and probably caused upstream services to timeout, and the errors were not handled properly, resulting in 500 errors to the client (load test). As autoscaling was set up, we can also conclude another important observation: autoscaling did not help, and our services would become unavailable due to bottlenecks!&lt;/p&gt;

&lt;h2&gt;
  
  
  Connection Pools and HTTP Clients
&lt;/h2&gt;

&lt;p&gt;The first thing I checked was the connection pools and HTTP clients set up so I could get an idea of the maximum parallel connections those services could open, how fast they would start rejecting new connections after all the current connections were busy, and how long they would wait for responses until they started to time out.&lt;/p&gt;

&lt;p&gt;In our case, we were not using &lt;a href="https://docs.spring.io/spring-framework/reference/web/webflux.html"&gt;WebFlux&lt;/a&gt;, so I didn’t want to start refactoring services and deal with breaking changes. I was more interested in first checking what I could optimize with minimal changes, preferably configuration only, and only performing larger changes if really needed. Think about the “Pareto rule”, “choose your battles wisely”, “time is money”, and so on. In this case, we were using the &lt;a href="https://www.baeldung.com/rest-template"&gt;Rest Template&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s review what a request flow would look like at a high level:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g_FWOTb_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2Axd8MvaSQnqD4dLiwOE98DA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g_FWOTb_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2Axd8MvaSQnqD4dLiwOE98DA.png" alt="" width="634" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, you can see the incoming request is handled by one of the servlet container’s (tomcat) threads, dispatched by Spring’s DispatcherServlet to the right controller, which calls a service containing some business logic. This service then calls a downstream remote service using an &lt;a href="https://hc.apache.org/httpcomponents-client-4.5.x/index.html"&gt;HTTP client&lt;/a&gt; through a traditional Rest template.&lt;/p&gt;

&lt;p&gt;The downstream service handles the request in a similar manner, but in this case, it interacts with MongoDB, which also uses a connection pool managed by &lt;a href="https://www.mongodb.com/docs/drivers/java-drivers/"&gt;Mongo Java Driver&lt;/a&gt; behind &lt;a href="https://spring.io/projects/spring-data-mongodb"&gt;Spring Data MongoDB&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first thing I noticed was that the setup was inconsistent, sometimes with missing timeout configurations, using default connection managers, and so on, making it a bit tricky to predict consistent behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeouts
&lt;/h3&gt;

&lt;p&gt;As a good practice, one should always check timeouts. Why? Not every application is the same; hence, the defaults might not fit your use cases. For example, database drivers tend to always have absurdly long timeouts by default for queries (such as infinite), as most simple applications might do some background task for performing a query once in a while and return the result to some task, job, or something similar. However, when we are talking about high-scalable and high-throughput systems, one must not wait forever for a query to complete; otherwise, if you have any issue with some specific collection, query, index, or anything like that blocks your DB instances, you will end up piling up requests and overloading all your systems very quickly.&lt;/p&gt;

&lt;p&gt;Think of it like a very long supermarket line with a very slow cashier, where the line keeps growing indefinitely and you are at the very end of the line. You can either wait forever and maybe get to the front of the line before the shop closes (and other people will keep queueing behind you), or you (and all the others) can decide after 3s to get out of that crowded place and come back later.&lt;/p&gt;

&lt;p&gt;Timeout is a mechanism to give the clients a quick response, avoiding keeping upstream services waiting and blocking them from accepting new requests. &lt;a href="https://medium.com/r?url=https%3A%2F%2Fresilience4j.readme.io%2Fdocs%2Fcircuitbreaker"&gt;Circuit breakers&lt;/a&gt;, on the other hand, are safeguards to avoid overloading your downstream services in case of trouble (connection drops, CPU overload, etc.). Circuit breakers are, for example, those waiters or waitresses who send customers back home without a chance to wait for a table when the restaurant is full.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection Pool
&lt;/h3&gt;

&lt;p&gt;Remote connections, such as the ones used for communicating with databases or Rest APIs, are expensive resources to create for each request. It requires opening a connection, establishing a &lt;a href="https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/"&gt;handshake&lt;/a&gt;, verifying certificates, and so on.&lt;/p&gt;

&lt;p&gt;Connection pools allow us to reuse connections to optimize performance and increase concurrency in our applications by maintaining multiple parallel connections, each in its own thread. Given certain configurations, they also give us the flexibility to queue requests for a certain amount of time if all connections from the pool are busy so they are not immediately rejected, giving our services more chances to serve all requests successfully within a certain period.&lt;/p&gt;

&lt;p&gt;You might have a quick read of this article for more information about connection &lt;a href="https://www.baeldung.com/httpclient-connection-management"&gt;pools&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So that’s more or less how it looks after the changes:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Bean
HttpClient httpClient(PoolingHttpClientConnectionManager connectionManager) {
    return HttpClientBuilder.create()
            .setConnectionManager( connectionManager )
            .build();
}

@Bean PoolingHttpClientConnectionManager connectionManager() {
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
    connectionManager.setMaxTotal( POOL_MAX_TOTAL );
    connectionManager.setDefaultMaxPerRoute( POOL_DEFAULT_MAX_PER_ROUTE );
    return connectionManager;
}

@Bean
ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
    factory.setConnectTimeout(CONNECT_TIMEOUT_IN_MILLISECONDS);
    factory.setReadTimeout(READ_TIMEOUT_IN_MILLISECONDS);
    factory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT);
    return factory;
}

@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
    RestTemplate restTemplate = new RestTemplateBuilder()
        .requestFactory(() -&amp;gt; clientHttpRequestFactory)
        .build();
    return restTemplate;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The beans above will ensure the HTTP client used by the Rest template uses a connection manager with a reasonable amount of max connections per route and max connections in total. If there are more incoming requests than we are able to serve with those settings, they will be queued by the connection manager until the connection request timeout is reached. If no attempt to connect is performed after the connection request timeout because the request is still in the queue, the request will fail. Read more about the different types of HTTP client timeouts &lt;a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.html"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Make sure to adjust the constants according to your needs and server resources. Be aware that one thread is open for each connection, and threads are limited by OS resources. Therefore, you can’t simply increase those limits to unreasonable values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s try it again!
&lt;/h2&gt;

&lt;p&gt;So there I was. Looking forward to another try after increasing the number of parallel requests handled by the HTTP clients and seeing a better overall performance of all services!&lt;/p&gt;

&lt;p&gt;But to my surprise, this happened:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IxEvNo1q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5284/1%2Al6-VV5uXNMgY1znSz3XJbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IxEvNo1q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5284/1%2Al6-VV5uXNMgY1znSz3XJbw.png" alt="" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Oc73S0i1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2644/1%2AQBM0rZF4ferFuEMhRa0Ryw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Oc73S0i1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2644/1%2AQBM0rZF4ferFuEMhRa0Ryw.png" alt="" width="800" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tOB5Jdu5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2A5gsN1d5OTz6xWHyDzAikkg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tOB5Jdu5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2A5gsN1d5OTz6xWHyDzAikkg.png" alt="" width="754" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So now our average latency has increased almost 8 times, and the number of errors has also increased! How come?!&lt;/p&gt;

&lt;h2&gt;
  
  
  MongoDB
&lt;/h2&gt;

&lt;p&gt;Luckily, I also had a monitoring setup for our MongoDB cluster, and there, it was easy to spot the culprit! A document was locked up by several concurrent write attempts. So, the changes indeed increased throughput, and now our DB was overloaded with so many parallel writes in the same document, which caused a huge amount of time waiting for it to be unlocked for the next query to update.&lt;/p&gt;

&lt;p&gt;You may want to read more about MongoDB concurrency and locks &lt;a href="https://www.mongodb.com/docs/manual/faq/concurrency/#:~:text=see%20consistent%20data.-,What%20type%20of%20locking%20does%20MongoDB%20use%3F,document%2Dlevel%20in%20WiredTiger"&gt;here&lt;/a&gt;.).&lt;/p&gt;

&lt;p&gt;As a consequence, the DB connection pool was busy queueing requests, and therefore, the upstream services also started to get their thread pools busy handling incoming requests due to the sync nature of rest templates waiting for a response. This increased CPU consumption in upstream services and caused higher processing times and failures, as we observed in previous graphics!&lt;/p&gt;

&lt;p&gt;As MongoDB monitoring pointed me to the exact collection containing the document that was locked and I had CPU profiling enabled (which I’ll describe in the next section), I could easily find the line code causing the lock through an unnecessary save() call in the same document at each service execution for updating one single field, which, to my surprise, never changed its value.&lt;/p&gt;

&lt;p&gt;Document locks are necessary for concurrency but are no good as they can easily start blocking your DB connections, and they usually indicate problems with either your code or collections design, so always make sure to review it in case you see some indication your documents are being locked.&lt;/p&gt;

&lt;p&gt;After removing the unnecessary save() call, things started looking better — but still not good.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qcDYPHUN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2264/1%2AbjpKEaRK5loIh1P717BueQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qcDYPHUN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2264/1%2AbjpKEaRK5loIh1P717BueQ.png" alt="" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gZyGI6au--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2ANjpQ6QHaCPlz9L9UMyb1HA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gZyGI6au--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2ANjpQ6QHaCPlz9L9UMyb1HA.png" alt="" width="610" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In comparison to the initial measures, the latency is higher, though the error rate dropped to almost 1/3 of the initial amount. Also, in comparison to the first try, it seems the errors are popping up slower than before.&lt;/p&gt;

&lt;p&gt;Before proceeding to fix the next bottleneck, let’s review one more thing. Ok, we had an issue in the code that caused the locks, but why did we let queries run for so long? Remember what I wrote initially at connection pools, HTTP clients, and timeout sections. The same applies here: remember to always review default values for your connections and timeouts. MongoDB allows you to overwrite defaults through its &lt;a href="https://www.mongodb.com/docs/drivers/java/sync/current/fundamentals/connection/connection-options/"&gt;connection options&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Connections will be created based on both minPoolSize and maxPoolSize. If queries take longer to be executed and new queries come in, new connections will be created until maxPoolSize is reached. From there, we can also define how long a query can wait to be executed with waitQueueTimeoutMS. If we are talking about DB writes, which was our case here, you should also review wtimeoutMS, which, by default, keeps the connection busy until the DB finishes the write. If setting a value different than the default (never timeout), you may also set a circuit breaker around the DB to ensure you don’t overload it with additional requests. If your DB cluster contains multiple nodes, distribute the load with reads by setting readPreference=secondaryPreffered. Be aware of &lt;a href="https://www.mongodb.com/docs/manual/core/read-isolation-consistency-recency/"&gt;consistency, read isolation, and recency&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU Profiling
&lt;/h2&gt;

&lt;p&gt;If you are working on performance issues, the first thing you should care about is profiling your application. This can be done locally using your favorite IDE or remotely attaching the profiler agent to your JVM process.&lt;/p&gt;

&lt;p&gt;Application profiling enables you to see which frames of your application consume the most processing time or memory.&lt;/p&gt;

&lt;p&gt;You can read more about Java profilers &lt;a href="https://www.baeldung.com/java-profilers"&gt;here&lt;/a&gt;. I used the &lt;a href="https://docs.aws.amazon.com/codeguru/latest/profiler-ug/setting-up.html"&gt;CodeGuru profiler&lt;/a&gt; from AWS in this case.&lt;/p&gt;

&lt;p&gt;See below an example of an application containing performance issues profiled with CodeGuru.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TXDLQhfh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6904/1%2AtzCbwLrMCvM_IyoIG2DWaw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TXDLQhfh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6904/1%2AtzCbwLrMCvM_IyoIG2DWaw.png" alt="" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The large frames indicate a large amount of processing time, and the blue color indicates namespaces recognized as your code. On top of that, sometimes you may have some recommendations based on a detected issue. However, don’t expect it to always point you precisely to the issues in your code. Focus on the large frames and use them to detect parts of the code that normally should not consume so much processing time.&lt;/p&gt;

&lt;p&gt;In the example above, one of the main issues seems to be creating SQS clients in the Main class. After fixing it, come back and check what the profiling results look like after some period of time monitoring the new code.&lt;/p&gt;

&lt;p&gt;In our case, the profiler indicated a couple of problematic frames in different applications, which caused bottlenecks and, as a consequence, the 500 errors and long latency in the previous graphics.&lt;/p&gt;

&lt;p&gt;In general, this either indicates low-performant code (e.g., strong encryption algorithms executed repeatedly) or leaks in general (e.g., the creation of a new object mapper in each request). In our case, it pointed to some namespaces, and after analyzing them, I could find opportunities for caching expensive operations, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thymeleaf Cache
&lt;/h2&gt;

&lt;p&gt;This was a funny one. A cache is always supposed to speed up our code execution, as we don’t need to obtain a resource for the source again, right? Right…?&lt;/p&gt;

&lt;p&gt;Yes, if configured properly!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.thymeleaf.org/"&gt;Thymeleaf&lt;/a&gt; serves frontend resources in this service, and it has cache enabled for static resources based on content. Something like the following properties:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.resources.chain.enabled=true
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;However, there are two issues introduced with these three lines.&lt;/p&gt;

&lt;p&gt;1- Caching is enabled based on resource content. However, with each request, the content is read from the disk over and over again so its hash can be recalculated, as the result of the hash calculation for the cache itself is not cached. To solve this, don’t forget to add the following property:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.resources.chain.cache=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;2- Unfortunately, the service is not using any base path for unifying the resolution of our static resources, so basically, Thymeleaf would try by default to load every link as a static resource from disk, even though they were just controller paths, for example. Keep in mind that disk operations are, in general, expensive.&lt;/p&gt;

&lt;p&gt;As I didn’t want to introduce an incompatible change by moving all static resources to a new directory within the resources folder, as it would cause link changes, and I had very well-defined paths for the static resources, I could simply solve it with &lt;a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.html#setOptimizeLocations(boolean)"&gt;setOptimizeLocations()&lt;/a&gt; from ResourceHandlerRegistration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disabling Expensive Debug Logs
&lt;/h2&gt;

&lt;p&gt;Another common mistake is to enable excessive logging, especially logs that print too much too often (e.g., often full stack trace logging). If you have high throughput on your systems, make sure to set up an appropriate log level and log only the necessary information. Review your logs frequently and evaluate if you want to be alerted to warnings and errors when you have your logs clean (e.g., no wrong log levels for debug/trace info).&lt;/p&gt;

&lt;p&gt;In this specific case, we had one log line logging a full stack trace for common scenarios. I disabled it as it was supposed to be enabled just for a short period of time for debugging purposes and disabled afterward but it was probably just forgotten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto Scaling Tuning
&lt;/h2&gt;

&lt;p&gt;Auto-scaling settings are easy to get working, but it can be tricky to get them working optimally. The basic thing you can do is enable auto-scaling based on CPU and Memory metrics. However, knowing your services in terms of how many requests per second they are able to handle can help you scale horizontally before your services start to degrade performance.&lt;/p&gt;

&lt;p&gt;Check possible different metrics you may want to observe for scaling, set reasonable thresholds, fine-tune &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/userguide/service-configure-auto-scaling.html"&gt;scale-in and scale-out cooldown periods&lt;/a&gt;, define minimum desired instances according to your expected load, and define a maximum number of instances to avoid unexpectedly high costs. Know your infrastructure and your service implementations inside out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving Another Try
&lt;/h2&gt;

&lt;p&gt;Performing the same load test one more time yielded the following results in comparison with the initial results:&lt;/p&gt;

&lt;h3&gt;
  
  
  Count: Sum
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YYLKm0mW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4348/1%2AycZtGj8omrg6U-zTUuF3_Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YYLKm0mW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4348/1%2AycZtGj8omrg6U-zTUuF3_Q.png" alt="" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are now handling more than double the number of requests within the same 15 minutes of testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integration Latency: Average
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--58LzhGBn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4820/1%2AcerDGSP-5qS_Nh7I1o7Nuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--58LzhGBn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4820/1%2AcerDGSP-5qS_Nh7I1o7Nuw.png" alt="" width="800" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The average integration latency in the service, which had load applied, was reduced more than twice compared to before. Meanwhile, downstream services remained with almost constant latency during the tests compared to before, so no more domino effect was observed.&lt;/p&gt;

&lt;h3&gt;
  
  
  5XXError: Sum
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--luEDfQh9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4492/1%2AWcBxPrf_kMX28HEYh855dA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--luEDfQh9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4492/1%2AWcBxPrf_kMX28HEYh855dA.png" alt="" width="800" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More importantly, errors were gone. The remaining errors we see on the graphic on the right are unrelated to the load test, as we can see in the following report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rLoY21W1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/3020/1%2AX-VqNAADIbJFmNnkTnb6nA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rLoY21W1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/3020/1%2AX-VqNAADIbJFmNnkTnb6nA.png" alt="" width="800" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we can see that auto-scaling changes helped us reduce the average response time to the normal state and keep it stable after about 10 minutes of the test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--T4ln1AzF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/7136/1%2Ai5pZYUu-oudsFVN-1ftEuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T4ln1AzF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/7136/1%2Ai5pZYUu-oudsFVN-1ftEuw.png" alt="" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Are we done? Of course not.&lt;/p&gt;

&lt;p&gt;These optimisations took me about 24 hours of work in total, but they should be performed regularly in multiple systems and different parts of them. However, when considering a large enterprise, such work can quickly become very expensive.&lt;/p&gt;

&lt;p&gt;Choosing a good balance between keeping it as it is and becoming obsessed with optimizing every millisecond is tricky, and you should keep in mind that such optimizations bring against opportunity costs.&lt;/p&gt;

&lt;p&gt;Do not forget that it’s not only about tuning services to be performant under high load but also making sure your services can produce consistent and correct results under normal conditions as well (e.g., such issues as higher I/O dependencies can lead to “random” unexpectedly longer response times if some jobs are being performed in the background on the operational system of your service instance, for example).&lt;/p&gt;

&lt;p&gt;Finally, I often see developers tend to use frameworks and infrastructure without knowing their internals, and this behavior introduces several issues without being noticed. Ensure you understand how your systems behave, what bottlenecks they create, what possible security issues could be exploited, and which settings are available to optimize them to your needs.&lt;/p&gt;

&lt;p&gt;I hope this article helps you set the mindset of caring about such aspects of your systems. Good luck!&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>performance</category>
      <category>testing</category>
      <category>profiling</category>
    </item>
    <item>
      <title>DocumentDB Vacuum Locks</title>
      <dc:creator>Felipe Malaquias</dc:creator>
      <pubDate>Sat, 23 Mar 2024 12:25:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/documentdb-vacuum-locks-57fn</link>
      <guid>https://dev.to/aws-builders/documentdb-vacuum-locks-57fn</guid>
      <description>&lt;h2&gt;
  
  
  Beware possible locks on large updates/deletions
&lt;/h2&gt;

&lt;p&gt;Historically, traditional databases dealt with writes with pessimistic locks on records during writes to avoid inconsistency, which had the obvious drawback of being unable to handle concurrency properly, as transactions could fail.&lt;/p&gt;

&lt;p&gt;This is solved by &lt;a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control"&gt;MVCC&lt;/a&gt; (Multiversion Concurrency Control), by creating a new version of a record on every update, circumventing the need to lock records, and allowing concurrency (see &lt;a href="https://www.youtube.com/watch?v=iM71d2krbS4"&gt;this&lt;/a&gt; video from Cameron McKenzie for a nice and simple illustrated explanation).&lt;/p&gt;

&lt;p&gt;However, to clean up the old versions, a vacuum process must run in the background, which may cause locks in your complete collection, bottlenecks, and possibly unexpected downtimes in your application.&lt;/p&gt;

&lt;p&gt;There is no permanent fix for this at the moment, but if you need to perform such updates, you may contact support and ask them to disable the process that reclaims unused storage space. This will not negatively impact your workload, and space reclaimed by the garbage collector will continue to be recycled. However, the size of your collections will never decrease, even if a significant amount of data has been deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The good news&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AWS is currently working on a fix for it, which may be available at any time, so you should keep an eye on the &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/release-notes.html"&gt;DocumentDB release notes&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is how we experienced it
&lt;/h2&gt;

&lt;p&gt;On a lovely Tuesday morning, we reset one of our Kafka topics (~71 GB) to re-consume all our data for a particular domain to aggregate it with new fields in our database. All messages were successfully consumed and written in the primary DB instance in a few minutes as expected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--a2y5x31n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2AfiPdi6OEk4nb2aO6tF39Qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a2y5x31n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2AfiPdi6OEk4nb2aO6tF39Qg.png" alt="" width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What we did not expect, though, are those waves of latency increase in our workload hours after the records were consumed and initially without much of a pattern until approx. 5 pm:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KAjUjpEd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2948/1%2ALxNFDKkmQ9hZSSm4E4TnWQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KAjUjpEd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2948/1%2ALxNFDKkmQ9hZSSm4E4TnWQ.png" alt="" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those were all caused by locks on a particular collection in the read replicas as shown by the pink bars in the &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/performance-insights.html"&gt;Document performance insights&lt;/a&gt; metrics below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Hk8DrqNE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6504/1%2ACqNo7oVd6SCRw-KUs5UzCQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Hk8DrqNE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6504/1%2ACqNo7oVd6SCRw-KUs5UzCQ.png" alt="" width="800" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you see, the locks were gone after around 11 pm, matching the end of the DocumentDB freeable memory metrics changes below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cjK77wPs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2AmEzDP4eWYU-zFr3BCvyhKg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cjK77wPs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/2000/1%2AmEzDP4eWYU-zFr3BCvyhKg.png" alt="" width="761" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reaching out to AWS support, they investigated the issue. They confirmed it was caused by the process of reclaiming unused space during the garbage collection on the vacuum process. This process must be synchronized between the writer and readers because the readers might still have in-flight transactions that can see the deleted data. In some rare circumstances and the presence of a large amount of reclaimable data, this synchronization can adversely affect the workload on the replicas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Be aware of the MVCC strategy and check how it may affect your database (not only DocumentDB) in case of large updates as described above, and probably most importantly, always test it in staging first ;)&lt;/p&gt;

&lt;p&gt;Also, be aware of the known issue with the DocumentDB vacuum process and watch the &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/release-notes.html"&gt;release notes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>documentdb</category>
      <category>mvcc</category>
      <category>aws</category>
      <category>database</category>
    </item>
  </channel>
</rss>
