<?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: Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</title>
    <description>The latest articles on DEV Community by Yoshiki Fujiwara(藤原 善基)@AWS Community Builder (@yoshikifujiwara).</description>
    <link>https://dev.to/yoshikifujiwara</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1143688%2F2e0886ff-292c-4e8f-a588-bc7629c2321b.jpeg</url>
      <title>DEV Community: Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</title>
      <link>https://dev.to/yoshikifujiwara</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yoshikifujiwara"/>
    <language>en</language>
    <item>
      <title>Turning a Step Functions workflow into AI agent tools through AgentCore Gateway</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Sun, 01 Nov 2026 12:35:21 +0000</pubDate>
      <link>https://dev.to/aws-builders/turning-a-step-functions-workflow-into-ai-agent-tools-through-agentcore-gateway-4ld7</link>
      <guid>https://dev.to/aws-builders/turning-a-step-functions-workflow-into-ai-agent-tools-through-agentcore-gateway-4ld7</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the FSx for ONTAP S3 Access Points serverless patterns series&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The update this post is about
&lt;/h2&gt;

&lt;p&gt;On October 29, 2026, &lt;strong&gt;AWS Step Functions workflows became usable as AI agent tools through Amazon Bedrock AgentCore Gateway&lt;/strong&gt;. The agent decides when to run a workflow and what input to pass; Step Functions runs each step as you defined it. You can turn existing Standard / Express workflows into tools without rewriting them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Announcement: &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/step-functions-workflows-ai-agent-tools/" rel="noopener noreferrer"&gt;AWS Step Functions workflows now available as AI agent tools (AWS What's New, 2026-10-29)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-step-functions.html" rel="noopener noreferrer"&gt;AWS Step Functions as Connector Target&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post applies that update to an FSx for ONTAP S3 Access Points file-processing workflow and adds it to the repository as a sample pattern. The key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard workflow (discover → process → report)&lt;/strong&gt; turned into tools via the connector&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The agent decides when to run; the state machine owns the order of the steps&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gateway and connector target described in CloudFormation&lt;/strong&gt;, so &lt;code&gt;sam deploy&lt;/code&gt; alone completes it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verified on a live ap-northeast-1 deploy&lt;/strong&gt; (DemoMode, 3/3 files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No performance or cost numbers&lt;/strong&gt; (it was a smoke test, not a measurement)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What this post covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What the Step Functions connector makes possible (within the official docs)&lt;/li&gt;
&lt;li&gt;The shape of the pattern I added, and how it differs from the existing AgentCore integration&lt;/li&gt;
&lt;li&gt;The design points that are easy to trip over&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this post does not cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Performance and cost numbers (I ran it live, but that was a smoke test, not a measurement)&lt;/li&gt;
&lt;li&gt;Express workflows and synchronous responses (&lt;code&gt;StartSyncExecution&lt;/code&gt;) — this pattern stays on Standard&lt;/li&gt;
&lt;li&gt;Cedar authorization on the input body — the official behavior is limited right now (see below)&lt;/li&gt;
&lt;li&gt;The agent implementation itself (Strands, etc.) — this post is about the tool-provider side&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why I did this
&lt;/h2&gt;

&lt;p&gt;This series has been building serverless patterns that process files over S3 Access Points. The processing itself is usually a Step Functions workflow: discover, process each file, report. That shape already runs in several patterns.&lt;/p&gt;

&lt;p&gt;Then this update landed: you can turn a workflow into an agent tool. A workflow that used to be triggered by an EventBridge schedule or by hand now gets an entry point for an agent. You keep the business logic as-is and move the launch and status-check to natural language. Reusing existing assets without rewriting them looked worthwhile, so I tried it in the context of the series.&lt;/p&gt;

&lt;p&gt;The post that got me started is below. It uses a purchase-order-and-approval scenario and goes as far as task tokens and Cedar, which made the behavior concrete.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.classmethod.jp/articles/step-functions-agentcore-gateway-tools/" rel="noopener noreferrer"&gt;[Update] You can now use AWS Step Functions workflows as AI agent tools via AgentCore Gateway (Classmethod, JA)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where this fits
&lt;/h3&gt;

&lt;p&gt;The subject of this series is a workflow that processes files on a file store (FSx for ONTAP) through S3 Access Points. The discover-process-report shape maps directly onto business document processing. AWS itself shows a concrete pairing of Step Functions with AgentCore in an &lt;a href="https://aws.amazon.com/blogs/architecture/automate-medical-coding-with-step-functions-and-amazon-bedrock-agentcore/" rel="noopener noreferrer"&gt;architecture blog on automating medical coding&lt;/a&gt;, and intelligent document processing — classifying and extracting from contracts, invoices, applications — is a recurring subject in the AWS &lt;a href="https://docs.aws.amazon.com/solutions/intelligent-document-processing-on-aws/" rel="noopener noreferrer"&gt;solution guidance&lt;/a&gt; as well. A few situations where this connector fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When you want to add a natural-language entry point to a file-processing batch.&lt;/strong&gt; A workflow you used to start on a schedule or by hand can now be started from a conversation ("summarize the unprocessed files in that folder") and followed to its result. The workflow internals stay as they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you want the steps fixed but the timing left to a person or an agent.&lt;/strong&gt; The order of classify, extract, notify and the business rules stay pinned in the state machine; only "when to run it and what input to pass" is handed to the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you want a human approval before a sensitive action.&lt;/strong&gt; With task tokens you can pause the workflow partway, wait for a person's decision, and resume on approval (human-in-the-loop). AWS shows a &lt;a href="https://aws.amazon.com/blogs/machine-learning/building-ambient-agents-with-amazon-bedrock-agentcore-from-event-driven-signals-to-human-in-the-loop-workflows/" rel="noopener noreferrer"&gt;reference flow&lt;/a&gt; that wires event-driven signals to human approval.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The readers I have in mind are roughly these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform / operations people&lt;/strong&gt; who run existing Step Functions workflows and want to add an agent entry point without rewriting the business logic — shifting start and result-checking toward a conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application / line-of-business developers&lt;/strong&gt; automating document processing who want business users to start and check runs themselves, while the fixed steps stay in the workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security-minded people&lt;/strong&gt; who want to bound what the agent can reach. Which state machine and which operations become tools is declared, so the agent's reachable surface is something you can read as code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This overlaps with building the agent integration yourself (writing the agent with something like Strands and authoring your own tools) and with visual workflow builders. I read the connector approach as the one with the least extra implementation when "there is already a Step Functions workflow" matches your situation. It is less about which is best and more about picking the entry point that fits the assets you already have.&lt;/p&gt;




&lt;h2&gt;
  
  
  What became possible
&lt;/h2&gt;

&lt;p&gt;AgentCore Gateway gained a built-in Step Functions connector (&lt;code&gt;connectorId: "states"&lt;/code&gt;). When you create a target, you pick the operations to expose and the state machine, and the Gateway generates the tool definitions. You write neither a Lambda nor an MCP server.&lt;/p&gt;

&lt;p&gt;Per the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-step-functions.html" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;, the connector exposes ten operations. Four of them (&lt;code&gt;StartExecution&lt;/code&gt; / &lt;code&gt;StartSyncExecution&lt;/code&gt; / &lt;code&gt;ListExecutions&lt;/code&gt; / &lt;code&gt;DescribeStateMachine&lt;/code&gt;) pin a state machine ARN on the target. The pinned ARN never appears in the tool arguments, so the agent cannot run a different state machine. The other six take an execution ARN or a task token the agent supplies.&lt;/p&gt;

&lt;p&gt;The division of labor: the agent decides when to run and what input to pass, while the ordered steps and the business rules stay on the state machine side.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern I added
&lt;/h2&gt;

&lt;p&gt;It lives at &lt;code&gt;solutions/genai/stepfunctions-agentcore-tools/&lt;/code&gt;. The subject is the S3 AP file-processing workflow that is familiar from this series.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fycabac5emjsh6vfie89b.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fycabac5emjsh6vfie89b.png" alt="An AI agent connects over MCP to Amazon Bedrock AgentCore; an AWS Step Functions Standard state machine runs the Discovery, Processing and Report AWS Lambda functions in order; Processing calls Amazon Bedrock; and the workflow reaches Amazon FSx for NetApp ONTAP through an Amazon S3 Access Point" width="800" height="2029"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: exposing the workflow as agent tools through the Step Functions connector (dark theme &lt;a href="https://raw.githubusercontent.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/main/docs/images/png/part8-stepfunctions-agentcore-en-dark@2x.png" rel="noopener noreferrer"&gt;here&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery&lt;/strong&gt;: scans the S3 AP and builds the list of files to process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing&lt;/strong&gt;: reads one file, summarizes/classifies with Bedrock, scores confidence, and attaches a human-review decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report&lt;/strong&gt;: aggregates, adds a data-classification label, and sends an SNS summary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a target named &lt;code&gt;workflow&lt;/code&gt;, the agent sees tools like &lt;code&gt;workflow___StartExecution&lt;/code&gt;. It starts the workflow, receives an &lt;code&gt;executionArn&lt;/code&gt;, checks status and result with &lt;code&gt;workflow___DescribeExecution&lt;/code&gt;, and reads how far it progressed with &lt;code&gt;workflow___GetExecutionHistory&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it differs from the existing AgentCore integration
&lt;/h3&gt;

&lt;p&gt;This repository already has a Lambda-target AgentCore integration. This one uses the connector, so the target is a different thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda target: the Lambda becomes the tool; you write the tool definition yourself&lt;/li&gt;
&lt;li&gt;Connector: the state machine's operations become the tools; the Gateway generates the definitions; the Lambda is only invoked as a workflow Task and is not exposed to the Gateway&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  It completes in IaC
&lt;/h3&gt;

&lt;p&gt;The Gateway and the connector target are available as CloudFormation resources (&lt;code&gt;AWS::BedrockAgentCore::Gateway&lt;/code&gt; and &lt;code&gt;AWS::BedrockAgentCore::GatewayTarget&lt;/code&gt;). So they go into the same template as the Lambdas, the state machine, and the role, and &lt;code&gt;sam deploy&lt;/code&gt; alone gets you all the way to the gateway's &lt;code&gt;tools/list&lt;/code&gt; — no separate command to create the gateway. The deploy script is a thin wrapper that runs &lt;code&gt;sam build&lt;/code&gt; and &lt;code&gt;sam deploy&lt;/code&gt; and checks &lt;code&gt;tools/list&lt;/code&gt; at the end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design points that are easy to trip over
&lt;/h2&gt;

&lt;p&gt;Points I noticed while building. Let me separate them by source. The connector configuration schema and the Gateway role IAM I confirmed myself, from the CLI and the CDK sample as above, and then on a live deploy (below). The same-region and pinning constraints are from the official docs. The Japanese mojibake and the Cedar item I did not exercise on this live run — I am citing another post's report.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Gateway and the state machine are in the same region.&lt;/strong&gt; The Gateway calls Step Functions in its own region. A state machine in another region cannot be wired up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One pin per operation.&lt;/strong&gt; To expose several workflows under the same operation, use one target per workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool descriptions are generic.&lt;/strong&gt; The shape of the &lt;code&gt;input&lt;/code&gt; JSON is not in the tool. Tell the agent in the system prompt or a skill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard and Express differ in which operations work.&lt;/strong&gt; I chose Standard because it keeps an execution history, so &lt;code&gt;ListExecutions&lt;/code&gt; and &lt;code&gt;GetExecutionHistory&lt;/code&gt; work. If you need a synchronous answer, that is a separate look at Express and &lt;code&gt;StartSyncExecution&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat task tokens as secrets.&lt;/strong&gt; &lt;code&gt;SendTaskSuccess&lt;/code&gt; and friends cannot be scoped by IAM; anyone holding a valid token can complete the task. This pattern does not expose them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input-body authorization is not expressible in Cedar right now.&lt;/strong&gt; A policy conditioned on the &lt;code&gt;input&lt;/code&gt; body (an amount, a reason) currently hits a type-evaluation error — the Classmethod post above reports the same observation. Keep body-level decisions on the state machine side. Per-tool allow/deny does work in Cedar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, each of these sits well with the point of the pattern: don't hand everything to the agent; pin the steps you want enforced into the workflow. If you want to branch on the input body, that was a Choice state in the workflow all along.&lt;/p&gt;

&lt;h3&gt;
  
  
  On Japanese input
&lt;/h3&gt;

&lt;p&gt;The Classmethod post reports that Japanese in the input passed through the Gateway can get mangled, with a workaround of escaping the Japanese in the JSON to &lt;code&gt;\uXXXX&lt;/code&gt; before sending. I have not confirmed this on my own deployment yet, so the docs record it as a reported known behavior together with the workaround.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running it on real infrastructure
&lt;/h2&gt;

&lt;p&gt;I deployed it to the Tokyo region in DemoMode and exercised the agent-facing path end to end. The gateway's &lt;code&gt;tools/list&lt;/code&gt; showed all five tools; &lt;code&gt;StartExecution&lt;/code&gt; returned an &lt;code&gt;executionArn&lt;/code&gt;, &lt;code&gt;DescribeExecution&lt;/code&gt; gave me status and result, &lt;code&gt;GetExecutionHistory&lt;/code&gt; showed the step transitions, and &lt;code&gt;ListExecutions&lt;/code&gt; listed the runs. The workflow succeeded, all three files were processed, and the category and data-classification labels came through. The Gateway and connector target came up with the stack create and went away with the stack delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  On completeness
&lt;/h2&gt;

&lt;p&gt;This pattern is scoped to exposing a Standard workflow as tools. It does not include Express synchronous responses, task-token human-in-the-loop, or a Cedar build-out. I confirmed each is possible in the official docs, but cramming all of them into one sample blurs the point, so I drew the boundary.&lt;/p&gt;

&lt;p&gt;There are no performance or cost numbers either. I did run it live, but that was a smoke test, not a measurement — running once and running under load are different things. When I measure, I will write it up with the environment (region, model, file count, date) attached.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Being able to turn an existing Step Functions workflow into agent tools without rewriting it was a welcome update for me. The split — steps in the workflow, launch decision with the agent — holds with no extra implementation.&lt;/p&gt;

&lt;p&gt;I ran it live end to end and confirmed I could start a workflow from the agent and follow its result. What is left is seeing the Japanese mojibake and the Cedar behavior with my own eyes, and measuring performance and cost under load. Once I do that, I will write the follow-up with numbers. The use cases are something I want to dig into from there too.&lt;/p&gt;

&lt;p&gt;The implementation is on GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns" rel="noopener noreferrer"&gt;FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous (a published S3 AP post): &lt;a href="https://dev.to/aws-builders/from-serverless-patterns-to-field-ready-reference-architecture-fsx-for-ontap-s3-access-points-dhj"&gt;the 28-patterns post&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this helps anyone who wants to reach an existing Step Functions workflow from an agent.&lt;/p&gt;

&lt;p&gt;Bye now!!&lt;/p&gt;

&lt;p&gt;Yoshiki Fujiwara&lt;/p&gt;

</description>
      <category>aws</category>
      <category>fsx</category>
      <category>stepfunctions</category>
      <category>agentcore</category>
    </item>
    <item>
      <title>Modernizing an App on Amazon FSx for NetApp ONTAP: Four .NET / Java / Python Candidates, Plus Containers and AI — Six Paths Measured (Once per Case)</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Sat, 31 Oct 2026 02:08:18 +0000</pubDate>
      <link>https://dev.to/aws-builders/modernizing-an-app-on-amazon-fsx-for-netapp-ontap-four-net-java-python-candidates-plus-3lid</link>
      <guid>https://dev.to/aws-builders/modernizing-an-app-on-amazon-fsx-for-netapp-ontap-four-net-java-python-candidates-plus-3lid</guid>
      <description>&lt;h2&gt;
  
  
  Changing the runtime and the access path without moving the data: how authentication, authorization, and failures differ
&lt;/h2&gt;

&lt;p&gt;A long-lived business application often keeps the data it reads and writes on a file share. For rebuilding such an application on a newer language or runtime, Microsoft Learn lists dependencies reaching end of support, new security or compliance requirements, and performance or scalability limits as the signals (&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/porting/" rel="noopener noreferrer"&gt;Upgrade .NET apps overview&lt;/a&gt;). Its examples include moving a Windows-only .NET Framework app to cross-platform modern .NET, and beyond that, containerizing the app or replacing components with managed services.&lt;/p&gt;

&lt;p&gt;When you rebuild an application, what usually stalls the effort is not the application itself but what to do with the data it has been reading and writing. Moving the data to a new store brings along downtime during the migration, redesigning permissions, re-pointing reference paths, and re-validating on the destination. This bundle is what stops modernization.&lt;/p&gt;

&lt;p&gt;Not moving the data removes that whole bundle, because it decouples rebuilding the app from migrating the data. You get three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You avoid the migration work itself.&lt;/strong&gt; No downtime, no permission redesign, no path re-pointing, no destination re-validation. You can focus only on changing the app's language or runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data stays a single source of truth that several runtimes share at once.&lt;/strong&gt; No extra copies to keep in sync. (Keeping the data in one place so copies and synchronization are unnecessary is the single-source-of-truth, SSOT, idea.) This article measured six paths reaching the same single volume, with the data unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can choose the language and runtime independently of where the data lives.&lt;/strong&gt; The same data is reachable from .NET, Java, or Python, and from EC2, Lambda, or EKS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters when the existing data is large and costly to move, when the permission design is complex and you would rather not rebuild it on the destination, when several users or services use the same data at once, or when you cannot take the downtime a migration needs.&lt;/p&gt;

&lt;p&gt;Amazon FSx for NetApp ONTAP (FSx for ONTAP below; NetApp's ONTAP as an AWS-managed shared storage service) makes this possible, because it lets you use one volume through several doors: NFS, SMB, and the S3 API. AWS documents that S3 Access Points let you read and write file data with S3 object operations, without application changes and without moving the data out of file storage, and that you can keep using NFS and SMB alongside (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/accessing-data-via-s3-access-points.html" rel="noopener noreferrer"&gt;Accessing your data via Amazon S3 access points&lt;/a&gt;). On the container side, the Amazon EKS User Guide describes using NetApp Trident so that FSx for ONTAP serves as the storage behind Kubernetes persistent volumes (&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/fsx-ontap.html" rel="noopener noreferrer"&gt;Use high-performance app storage with FSx for NetApp ONTAP&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;But when you add access paths without moving the data, who authenticates, where authorization happens, and how failures return change per path. Add a path without knowing this, and you find the difference afterward. So this article measures reaching the same data through six paths and how authentication, authorization, and the shape of failures swap per path.&lt;/p&gt;

&lt;p&gt;Last time I took one .NET Framework app and, keeping its business logic unchanged while renewing only the language and runtime — the kind of migration AWS calls Refactor — rebuilt it step by step down to AWS Lambda without moving its data, and measured how SMB, NFS, and S3 Access Points differ when opening the same volume. This article continues from there by adding the runtimes and paths that reach the data. I measured SMB last time, so I did not measure it this time.&lt;/p&gt;

&lt;p&gt;Even after the app migration, the access path changes with how you run the app in the cloud: whether the same data is mounted from EC2, read with the S3 API from Lambda, or used from a container on EKS. This time I measure that next step after the migration — choosing the access path — by adding runtimes.&lt;/p&gt;

&lt;p&gt;I measured the following six paths. I call E-1 and E-2 together E.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What uses the volume, and how&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;A .NET 10 app on a Linux EC2 instance mounts the volume over NFS and reads and writes it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;A Java (Spring) app on a Linux EC2 instance mounts the volume over NFS and reads and writes it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;A Python Lambda reads and writes the volume through S3 Access Points (the S3 API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C-REST&lt;/td&gt;
&lt;td&gt;A Python Lambda reads the volume through the ONTAP REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Containers on EKS write to persistent volumes that Trident provisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-1&lt;/td&gt;
&lt;td&gt;Athena (an analytics service) reads Parquet files with SQL through S3 Access Points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-2&lt;/td&gt;
&lt;td&gt;Amazon Bedrock Knowledge Bases (a RAG service) ingests documents through S3 Access Points, and a filter inside a web app narrows the search results to the user's permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Four of them (A, B, C, C-REST) ran the same file-processing job, reimplemented in .NET, Java, and Python, against the same single volume. Because the job is the same, these four can be compared with the difference attributed to the path. I call each reimplementation a "candidate" in this article.&lt;/li&gt;
&lt;li&gt;D and E are not the same job. I tested different workloads, in other repositories, in other disposable environments. I do not compare them with A through C-REST; I report each path's behavior on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not run the same job over all six paths.&lt;/p&gt;

&lt;p&gt;First, the premises. All six paths ran on the same single file system (Single-AZ, ap-northeast-1; an existing verification file system, separate from the one I measured in stages 0 to 3 of the previous article), using disposable SVMs, volumes, and other resources I created. I read the ONTAP version in the measurements for candidates A, B, C-REST, and path D (across several days): 9.18.1 (NetApp Release 9.18.1P6). The C and E measurements did not read it and carried the value over. A different version can change a result, so read the results for C and E as results whose version I did not confirm at the time. A "case" here is a check fixed beforehand for each path (that it runs as expected, that it fails as expected on a deny or an unreachable target, and so on), and I ran each case once. I did not repeat runs to confirm reproducibility. None of these values is a guarantee for production.&lt;/p&gt;

&lt;p&gt;For A through C-REST, the job I measured is the core of DocIntake, the same sample app as last time. It is not a real business app or a production app. It has no screen (UI) and no web layer: a minimal batch job that reads files on the volume, processes them, and writes a result, reimplemented per language. The results do not carry over to an app with a UI.&lt;/p&gt;

&lt;p&gt;You may be wondering about these three points.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When the same file-processing job runs in languages other than .NET (Java, Python) over NFS, S3 Access Points, and ONTAP REST API, what changes in authentication, authorization, and how failures return?&lt;/li&gt;
&lt;li&gt;When containers on EKS, or analytics and RAG services, use the same volume, what could I confirm, and what is outside this article?&lt;/li&gt;
&lt;li&gt;Which of the six paths should I choose, and on what basis?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;I found three things.&lt;/strong&gt; I answer the three questions above in this order.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The answer to the first question is that the authentication principal, the network boundary, the authorization principal, and how failures return all change per path. For example, when access was denied, NFS (A and B) returned an OS permission error, S3 Access Points (C) returned HTTP 403 (AccessDenied), and ONTAP REST API (C-REST) returned HTTP 403 with an ONTAP error code. The four candidates in .NET, Java, and Python ran their designed cases, without moving the data, against disposable volumes on the same FSx for ONTAP file system.&lt;/li&gt;
&lt;li&gt;The answer to the second question has three parts. On D, I wrote data from a verification pod on EKS to iSCSI and NFS persistent volumes that I provisioned with Trident, and the same data was readable after a pod restart and after a reschedule to another node. On E-1, Athena read Parquet through S3 Access Points and the aggregation matched the values I computed from the source data. On E-2, a filter inside the web app removed documents the user was not permitted to see from the Bedrock Knowledge Bases search results, according to the user's permissions. All three are small-scale results for workloads other than A through C-REST.&lt;/li&gt;
&lt;li&gt;The answer to the third question is that authorization happens in a different place per path, so choose by the kind of client you use and where you want authorization to happen. The evidence is the tables "The paths at a glance" and "How failures return per path, and what was confirmed for D and E". I do not rank them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you can take back to your own environment is this. Each time you add a runtime or a path to the same volume, re-measure on the new path who authenticates (the OS, IAM, an ONTAP credential, the app), where the network goes (an NFS mount, the public S3 endpoint, a management LIF inside the VPC), where authorization is decided (OS permissions, IAM, ONTAP, a filter in the app), and how a failure returns (a permission error, an HTTP status, an ONTAP error code, removal from the search results).&lt;/p&gt;

&lt;h3&gt;
  
  
  Intended readers, and what this article does not answer
&lt;/h3&gt;

&lt;p&gt;The intended readers are SAs, application developers, storage administrators, and Partner/SI engineers who are modernizing applications on AWS, or considering it. Not only those already using FSx for ONTAP, but also those still choosing a destination. I picture the moment of being stuck on "I want to change the language or runtime, but what do I do with the data?", or of choosing between NFS, S3 Access Points, ONTAP REST API, and Trident.&lt;/p&gt;

&lt;p&gt;This article does not answer the following, because I did not measure them or measured them under different conditions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance (I ran no measurement with defined performance conditions).&lt;/li&gt;
&lt;li&gt;Guarantees for production (every value is one observation per case).&lt;/li&gt;
&lt;li&gt;Multi-AZ behavior (I built only Single-AZ).&lt;/li&gt;
&lt;li&gt;Behavior of an app with a UI (the A through C-REST sample has no UI).&lt;/li&gt;
&lt;li&gt;Large data or long-running operation (I used only small datasets).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reading order
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;To get the conclusion only, "I found three things" in this section is enough.&lt;/li&gt;
&lt;li&gt;To see the differences between paths at a glance, read "The paths at a glance" and "How failures return per path, and what was confirmed for D and E".&lt;/li&gt;
&lt;li&gt;To decide which path to choose, read "How to choose a path".&lt;/li&gt;
&lt;li&gt;Per-path detail is in the sections for candidates B, C, C-REST, path D, and path E.&lt;/li&gt;
&lt;li&gt;The axes this article does not cover are collected in "What this article covers / does not cover".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this article covers / does not cover
&lt;/h2&gt;

&lt;p&gt;This article reports six paths, each measured once per case in its own environment. It is not an exhaustive comparison.&lt;/p&gt;

&lt;p&gt;What I cover&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How authentication, network, authorization, and failure behavior differ when six paths use the same volume. A, B, C, and C-REST ran the same job over different paths, so I can attribute the difference to the path. D and E are different workloads, so I report each path's behavior on its own.&lt;/li&gt;
&lt;li&gt;How the failure cases (deny, unreachable, invalid identifier) returned on each path. What returns differs per path (a permission error, HTTP 403, an ONTAP error code, a connection timeout).&lt;/li&gt;
&lt;li&gt;Each case was measured once. Where I got through by hand, and where I did not retrieve a value, I say so on the spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I do not cover&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance. The purpose was to measure behavior differences per path, and I ran no measurement with defined performance conditions. Performance and scale are tracked in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/38" rel="noopener noreferrer"&gt;#38&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Multi-AZ and other ONTAP versions. I built only a Single-AZ configuration, and the measurements that read the ONTAP version read 9.18.1P6. Path D's Multi-AZ switchover and same-file concurrent-write consistency are tracked in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Container-Datastore-Patterns/issues/17" rel="noopener noreferrer"&gt;#17 in Container-Datastore&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Response time. I did not measure path E-2's response time or the natural expiry of the Lambda cache (waiting five minutes); they are tracked in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Agentic-Access-Aware-RAG/issues/209" rel="noopener noreferrer"&gt;#209&lt;/a&gt;. Path E-1's behavior when the domain controller is unreachable is &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Lakehouse-Integrations/issues/155" rel="noopener noreferrer"&gt;#155 in Lakehouse&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;SMB. I measured it in the previous article. I also do not cover containers on ECS or Windows containers that mount over SMB. For path D, I tested only one path, EKS with Trident.&lt;/li&gt;
&lt;li&gt;Reconciling with invoices. Costs are estimates for approved disposable environments, pre-bill values.&lt;/li&gt;
&lt;li&gt;Whether this applies to real data. E-1 and E-2 were measured with non-confidential synthetic data and placeholder SIDs. Data classification, retention policy, and any legal or compliance judgment before applying it to production business data or regulated data are outside this article.&lt;/li&gt;
&lt;li&gt;Whether to use the RAG's generated output. E-2 measured only the permission-based narrowing of search results; whether a generated answer built from the retrieved documents may be used directly for a decision (clinical or otherwise) needs separate consideration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Terms used in this article
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;One-line meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Path&lt;/td&gt;
&lt;td&gt;The route by which a runtime reaches a volume on FSx for ONTAP. In this article there are six: A, B, C, C-REST, D, and E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;The core processing of the sample app DocIntake: a minimal batch unit that reads a document from an intake folder, processes it, and writes a result report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Candidate&lt;/td&gt;
&lt;td&gt;A different implementation of the same job. Used only for A, B, C, and C-REST. D and E are not the same job, so I do not call them candidates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same job&lt;/td&gt;
&lt;td&gt;Running the job above as the same processing even when the language or the path changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFS&lt;/td&gt;
&lt;td&gt;The protocol Linux uses for file sharing. This article uses NFS 4.1 with AUTH_SYS (a simple authentication where the client claims a uid)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Access Points&lt;/td&gt;
&lt;td&gt;A door that reads and writes an FSx for ONTAP volume through the S3 API. AWS IAM, the access-point policy, and a file-system user authorize requests. This is an S3-compatible door, not an Amazon S3 bucket itself. The backing store is the same ONTAP volume as NFS/SMB, so reaching one volume through several protocols shares its throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP REST API&lt;/td&gt;
&lt;td&gt;A REST door for managing and operating ONTAP. Authentication is an ONTAP credential and ONTAP authorizes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management LIF&lt;/td&gt;
&lt;td&gt;ONTAP's network interface for management. It is reachable only from inside the VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVM&lt;/td&gt;
&lt;td&gt;ONTAP's virtual storage server. In this article I created a disposable SVM per verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trident&lt;/td&gt;
&lt;td&gt;NetApp's CSI driver, which provisions Kubernetes persistent volumes (PVs) as FSx for ONTAP volumes and LUNs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PV and PVC&lt;/td&gt;
&lt;td&gt;A PV is a persistent volume Kubernetes manages; a PVC is a pod's claim for one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;An approach in which a generative model answers using documents retrieved by search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SID&lt;/td&gt;
&lt;td&gt;A value that identifies a Windows user or group. In the RAG verification, the SIDs that permit each document are matched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fail-closed&lt;/td&gt;
&lt;td&gt;Erring toward the safe (closed) side when information is missing. In this article it refers to E-2's runtime behavior: a user with no permission information gets all search results denied&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The delta from the previous article
&lt;/h2&gt;

&lt;p&gt;The previous article (&lt;a href="https://dev.to/aws-builders/keeping-the-data-where-it-is-modernizing-only-the-app-from-net-framework-to-lambda-what-changes-1p4o"&gt;the previous article in this series&lt;/a&gt;) rebuilt one .NET Framework app from stage 0 to stage 3 and measured how file handling (case sensitivity, exclusive locking, write visibility, and so on) differs as the ways to open the same volume increased across SMB, NFS, and S3 Access Points. It had one implementation (.NET), and its axis was how file handling changes when the protocol changes.&lt;/p&gt;

&lt;p&gt;This time three axes change. One is more languages (Java and Python in addition to .NET). Another is a new access path, the ONTAP REST API. With these two, what I look at moves from file handling to authentication, network, authorization, and how failures return. The third is workloads other than the job itself that use the same volume: containers on EKS (D) and analytics and RAG services (E), which I tested in other repositories.&lt;/p&gt;

&lt;p&gt;Candidate A (.NET / NFS) is the same lineage as stages 2 and 3 last time, so I keep it to a summary and point to the previous article and the issue that records its runtime verification (&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/42" rel="noopener noreferrer"&gt;#42&lt;/a&gt;). What I newly measured is B, C, C-REST, D, and E.&lt;/p&gt;

&lt;h2&gt;
  
  
  The paths at a glance
&lt;/h2&gt;

&lt;p&gt;Here are the six paths in one table. Only the four with "Yes" in the "Same job?" column can be compared with each other.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Same job?&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;How it reaches the volume&lt;/th&gt;
&lt;th&gt;Authentication and authorization&lt;/th&gt;
&lt;th&gt;Repository where tested&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Yes (DocIntake)&lt;/td&gt;
&lt;td&gt;.NET 10 / Linux EC2&lt;/td&gt;
&lt;td&gt;NFS 4.1 mount&lt;/td&gt;
&lt;td&gt;The OS (uid) authenticates; OS permissions authorize&lt;/td&gt;
&lt;td&gt;This repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Yes (DocIntake)&lt;/td&gt;
&lt;td&gt;Java / Spring worker / Linux EC2&lt;/td&gt;
&lt;td&gt;NFS 4.1 mount&lt;/td&gt;
&lt;td&gt;The OS (uid) authenticates; OS permissions authorize&lt;/td&gt;
&lt;td&gt;This repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Yes (DocIntake)&lt;/td&gt;
&lt;td&gt;Python 3.14 Lambda (outside the VPC)&lt;/td&gt;
&lt;td&gt;S3 Access Points (S3 API)&lt;/td&gt;
&lt;td&gt;AWS IAM authenticates; IAM and the access-point policy authorize&lt;/td&gt;
&lt;td&gt;This repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C-REST&lt;/td&gt;
&lt;td&gt;Yes (DocIntake)&lt;/td&gt;
&lt;td&gt;Python 3.14 Lambda (inside the VPC)&lt;/td&gt;
&lt;td&gt;ONTAP REST API (management LIF)&lt;/td&gt;
&lt;td&gt;An ONTAP credential (basic auth); ONTAP authorizes&lt;/td&gt;
&lt;td&gt;This repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;No (a verification pod using Trident PVs)&lt;/td&gt;
&lt;td&gt;EKS 1.35 / EC2 nodes&lt;/td&gt;
&lt;td&gt;PVs Trident provisions (iSCSI, NFS)&lt;/td&gt;
&lt;td&gt;Trident authenticates to the management LIF with a vsadmin credential and provisions the volumes&lt;/td&gt;
&lt;td&gt;Container-Datastore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-1&lt;/td&gt;
&lt;td&gt;No (an Athena SQL query)&lt;/td&gt;
&lt;td&gt;Athena (managed)&lt;/td&gt;
&lt;td&gt;S3 Access Points (S3 API)&lt;/td&gt;
&lt;td&gt;AWS IAM authenticates; IAM and a file-system user authorize&lt;/td&gt;
&lt;td&gt;Lakehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-2&lt;/td&gt;
&lt;td&gt;No (a Bedrock Knowledge Bases search)&lt;/td&gt;
&lt;td&gt;Bedrock Knowledge Bases and a web app&lt;/td&gt;
&lt;td&gt;S3 Access Points as the ingestion source&lt;/td&gt;
&lt;td&gt;Users authenticate with Cognito; a SID match inside the app decides per-document access&lt;/td&gt;
&lt;td&gt;Agentic-RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As a diagram: the top tier is the four candidates for the same job (A and B on Amazon EC2, C and C-REST on AWS Lambda), the bottom tier is the two paths for different workloads (D on Amazon Elastic Kubernetes Service, E on Amazon Athena and Amazon Bedrock), and both reach one Amazon FSx for NetApp ONTAP volume.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhglh0a8szivopiftwg3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhglh0a8szivopiftwg3.png" alt="Four candidates for the same job (A and B on Amazon EC2, C and C-REST on AWS Lambda) and two paths for different workloads (D on Amazon Elastic Kubernetes Service, E on Amazon Athena and Amazon Bedrock) all reaching one Amazon FSx for NetApp ONTAP volume" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 1: Six paths reaching one FSx for ONTAP volume. The top tier is the same job's candidates; the bottom tier is different workloads (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/8726146e66ce6c0a5152019e54c593bb18fedf9f/docs/_assets/images/png/cover-appmod-runtime-en-dark@2x.png" rel="noopener noreferrer"&gt;dark theme version&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification environment and method
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configuration (A, B, C, C-REST)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Region ap-northeast-1, Single-AZ. The ONTAP version is 9.18.1 (NetApp Release 9.18.1P6), read in the measurements for candidates A, B, C-REST, and path D. For C and E I carried the value over from the same file system and did not read it again in those measurements.&lt;/li&gt;
&lt;li&gt;I used the existing file system (an existing verification file system that I keep, separate from the one I measured in stages 0 to 3 of the previous article) as the host, and created a disposable SVM and volume per candidate. I did not touch the existing 5 SVMs and 31 volumes. After each verification I deleted the disposable resources and confirmed that the existing 5 SVMs and 31 volumes were still present.&lt;/li&gt;
&lt;li&gt;Candidates A and B mounted over NFS 4.1 from a Linux Amazon EC2 instance (Graviton arm64, AL2023). Candidate C reached FSx for ONTAP S3 Access Points from a Lambda outside the VPC, over the public S3 endpoint. Candidate C-REST reached the ONTAP management-LIF REST API from a Lambda inside the VPC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration (D, E-1, E-2)
&lt;/h3&gt;

&lt;p&gt;I measured D and E in other repositories, on other days. In each case I added disposable resources to the same file system as A through C-REST, and deleted them after verification.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What was disposable&lt;/th&gt;
&lt;th&gt;Confirmation that existing resources were untouched&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;A dedicated SVM (with its own vsadmin), an EKS cluster (Kubernetes v1.35), two m5.large nodes (Amazon Linux 2023), and the Trident add-on v26.6.2&lt;/td&gt;
&lt;td&gt;Before and after teardown, this file system's 5 backups, 5 SVMs, 31 volumes, and 72 ENIs in the VPC were identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-1&lt;/td&gt;
&lt;td&gt;One volume, one internet-origin S3 Access Point, a Glue database and table, and an Athena results bucket&lt;/td&gt;
&lt;td&gt;After teardown, the existing file system, its 5 SVMs, and its 8 access points were unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-2&lt;/td&gt;
&lt;td&gt;One volume, one internet-origin S3 Access Point, a Bedrock Knowledge Base, an S3 Vectors index, and a CDK stack including the web app&lt;/td&gt;
&lt;td&gt;After teardown, the existing file system, SVMs, access points, and volumes were unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I read the ONTAP version in the path D measurement (9.18.1P6) and did not read it in the E measurements. The SVM used for E-1 was joined to Active Directory, and the SVM used for E-2 was not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method
&lt;/h3&gt;

&lt;p&gt;For each path I fixed beforehand, as the success criteria, both the cases that run as expected and the failure cases (that fail as expected on a deny, an unreachable target, or an invalid identifier), and recorded the real responses. The failure cases are there to confirm not only "it ran" but also "the failure returns in the expected shape." If you only watch the happy path, you miss a request that should be denied slipping through, or an unreachable target quietly returning empty. Because the shape of a failure swaps per path (a deny as an OS permission error vs. HTTP 403; an unreachable target as an exception vs. a timeout), I measured the failure cases per path as much as the normal ones. Completion was judged not by a build or a call succeeding, but by each normal and failure case returning the expected real response. The real responses for A through C-REST are in a private record. For D and E they are in the public records of each repository, linked in "Resources".&lt;/p&gt;

&lt;p&gt;Each case was measured once, without repeating runs to confirm reproducibility. Where I got through by hand, and where I did not retrieve a value, I note it on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Candidate A (.NET / NFS): the previous article in summary
&lt;/h2&gt;

&lt;p&gt;Candidate A is a .NET 10 BackgroundService that processes files on a volume mounted over NFS 4.1. It is the same lineage as stages 2 and 3 last time; I confirmed four cases.&lt;/p&gt;

&lt;p&gt;Two findings, briefly. One is the systemd unit type: &lt;code&gt;Type=notify&lt;/code&gt; stalls startup because the worker sends no sd_notify, and the correct type is &lt;code&gt;Type=simple&lt;/code&gt;. The other is that the local promotion gate (NfsPromotionGate) validated &lt;code&gt;source_revision&lt;/code&gt; with a 64-hex (SHA-256) regex, rejecting a 40-hex Git SHA-1 commit id. Details are in the previous article and &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/42" rel="noopener noreferrer"&gt;#42&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Candidate B (Java / NFS): FileLock behavior is invisible without two processes
&lt;/h2&gt;

&lt;p&gt;Candidate B runs a Java/Spring background worker on Linux, processing a volume mounted over NFS 4.1. I confirmed file processing, lock acquire/release, cross-process lock contention, same-JVM lock overlap, a platform I/O error, and wait/retry, using two hosts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File processing processed a document on the NFS mount and wrote a report with the expected length and first line.&lt;/li&gt;
&lt;li&gt;File lock acquire/release recorded &lt;code&gt;acquired_released&lt;/code&gt;, report-write and claim-release for each item, with a clean Spring start, item success, normal stop, and exit 0.&lt;/li&gt;
&lt;li&gt;Cross-process lock contention ran two JVMs on the NFS mount. While the holder kept the lock, the contender got &lt;code&gt;tryLock=null&lt;/code&gt; and was classified &lt;code&gt;lock_contention&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Same-JVM lock overlap threw &lt;code&gt;OverlappingFileLockException&lt;/code&gt; when opened twice within one JVM, classified &lt;code&gt;same_jvm_contention&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A platform I/O error was recorded as &lt;code&gt;platform_io_error&lt;/code&gt; with operation, platform, and exception.&lt;/li&gt;
&lt;li&gt;Wait/retry: a bounded wait was measured on two hosts over NFS (below). Past the limit, a &lt;code&gt;null&lt;/code&gt; is still &lt;code&gt;lock_contention&lt;/code&gt;, and nothing is retried silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wait/retry was measured once each on two hosts (ONTAP 9.18.1P6, NFS 4.1, Amazon Linux 2023 arm64; the SVM's lease is 30 s, grace 60 s). The waiter took the claim 198 ms after the holder released it (250 ms retry interval); a 10 s limit ended as &lt;code&gt;lock_contention&lt;/code&gt; after 41 attempts; with no wait it was &lt;code&gt;lock_contention&lt;/code&gt; after one attempt. After &lt;code&gt;kill -9&lt;/code&gt; of the holder JVM the claim was taken 162 ms later; after a forced stop of the holder's host it was taken about 75 s after the stop request (a single observation that does not separate stop latency from lease expiry). During the measurement, a start-signal file placed on the NFS volume was not seen by the second host for about 30 s; the cause is unconfirmed, and the two-host start signal was switched to the process's standard output.&lt;/p&gt;

&lt;p&gt;A guide to setting the wait limit in production. When the holder releases normally, the claim was taken in a few hundred milliseconds, so a short limit is enough. But if you wait for recovery from a holder host failure, the lock only clears after the lease expires, so the limit must be longer than the lease (30 s) plus grace (60 s), about 90 s, or you give up before recovery. Decide by whether you prioritize responsiveness or not dropping work on a failure.&lt;/p&gt;

&lt;p&gt;One behavior stood out. A &lt;code&gt;null&lt;/code&gt; &lt;code&gt;java.nio.channels.FileLock&lt;/code&gt; (cross-process lock contention) is observable only with two processes. Within one JVM the platform throws &lt;code&gt;OverlappingFileLockException&lt;/code&gt; (&lt;code&gt;same_jvm_contention&lt;/code&gt;). The NFS mount enforced cross-process exclusion as expected.&lt;/p&gt;

&lt;p&gt;There was also a build-environment trap: Maven's default JDK on AL2023 did not accept release 21, and &lt;code&gt;JAVA_HOME&lt;/code&gt; had to point explicitly at Corretto 21, otherwise &lt;code&gt;mvn&lt;/code&gt; failed with "release version 21 not supported."&lt;/p&gt;

&lt;h2&gt;
  
  
  Candidate C (Python Lambda / FSx for ONTAP S3 Access Points): IAM authorizes
&lt;/h2&gt;

&lt;p&gt;Candidate C is a Python 3.14 Lambda that reads and writes volume objects through FSx for ONTAP S3 Access Points. On a disposable UNIX SVM and volume not joined to Active Directory, I created an internet-origin access point and reached it from a Lambda outside the VPC over the public S3 endpoint. I confirmed five cases; file locking was out of scope.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PutObject wrote an object.&lt;/li&gt;
&lt;li&gt;GetObject returned a read-back hash equal to the write input hash (roundtrip hash match).&lt;/li&gt;
&lt;li&gt;ListObjectsV2 returned a listing.&lt;/li&gt;
&lt;li&gt;Policy-condition deny: with an explicit Deny variant, a GetObject through the access point returned &lt;code&gt;AccessDenied&lt;/code&gt; / HTTP 403. A before/after control (writable before the Deny, denied after) isolated the policy effect.&lt;/li&gt;
&lt;li&gt;Invalid parser input classified invalid bytes locally. It is not a managed-Lambda result.&lt;/li&gt;
&lt;li&gt;File locking was out of scope because this operation set does not express file locking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The finding is that FSx for ONTAP S3 Access Point object operations (Get/Put/List) ran from a Lambda outside the VPC over an internet-origin access point on a UNIX SVM not joined to Active Directory, with no dependency on reachability to an AD domain controller. A deny returned through the access point as the exact &lt;code&gt;AccessDenied&lt;/code&gt; / HTTP 403 class, and the before/after control isolated the policy effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Candidate C-REST (Python Lambda / ONTAP REST API): ONTAP authorizes, plus the self-signed certificate and multipart traps
&lt;/h2&gt;

&lt;p&gt;Candidate C-REST is a Python 3.14 Lambda that reads files through the ONTAP REST API. It is the same Python Lambda as candidate C, but the access path is entirely different. Candidate C uses the public S3 endpoint and AWS IAM authorizes. C-REST has a Lambda inside the VPC reach the management-LIF REST API, authenticate with an ONTAP credential (basic auth), and let ONTAP authorize. I confirmed a read, recording the result, a write, a multi-call read, and four failure cases (ONTAP 9.18.1P6).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File listing read: a GET on the files resource returned a listing, HTTP 200, Accept &lt;code&gt;application/json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Recording the read result: the read result was recorded to a separate bucket, and its hash was independently confirmed to match the read's output hash.&lt;/li&gt;
&lt;li&gt;Write: 2.5 MB written in three calls. The ONTAP REST API creates a file with a multipart POST and then writes with PATCH at a byte offset (a JSON POST to create first returned HTTP 405). The read-back hash matched the write hash — but the read-back uses the same REST API, so it is not independent of the write path.&lt;/li&gt;
&lt;li&gt;Multi-call read: the size came from the parent directory listing, then four ranged reads reassembled the whole file and the hash matched.&lt;/li&gt;
&lt;li&gt;Single-call size limit: 1,048,576 bytes returned HTTP 200 and 1,048,577 returned HTTP 413 (no body), confirming the documented 1 MB per-request limit.&lt;/li&gt;
&lt;li&gt;Auth failure: a wrong credential returned HTTP 401, ONTAP error code 6691623, and no bytes were served.&lt;/li&gt;
&lt;li&gt;Authorization deny: with a read-only user, GET was allowed and a write returned HTTP 403, ONTAP error code 6, "not authorized for that command."&lt;/li&gt;
&lt;li&gt;Network unreachable: toward an unused address, a connection timeout was classified endpoint-unreachable, and no bytes were served.&lt;/li&gt;
&lt;li&gt;Invalid identifier: a valid volume identifier with a non-existent path returned HTTP 404. A malformed volume identifier instead returned HTTP 200 with an empty listing, so I measured the invalid identifier on the path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are four findings about the shape of the API when you call the ONTAP REST API from a client. A file is created with a multipart POST and then written with PATCH at a byte offset (a JSON POST returns HTTP 405). A read response returns two entries (&lt;code&gt;bytes_read&lt;/code&gt; and the data), so only the data is hashed. A JSON listing of a file path returns HTTP 400 "Not a directory", so the size comes from the parent directory listing. Exceeding the single-call read limit returns HTTP 413, which maps to its own class. These are recorded in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/73" rel="noopener noreferrer"&gt;#73&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is also a TLS finding, and it is not about S3 Access Points (Candidate C). When you reach FSx for ONTAP through S3 Access Points, an AWS-managed endpoint terminates TLS, so the client does not deal with a certificate (putting a certificate on a custom domain is AWS Certificate Manager territory, separate from any ONTAP-side certificate). The ONTAP REST API (Candidate C-REST), by contrast, calls the ONTAP management endpoint (the cluster-management LIF), where ONTAP itself — not an AWS-managed TLS endpoint — presents the certificate. ONTAP presents a self-signed certificate by default (&lt;a href="https://kb.netapp.com/on-prem/ontap/Ontap_OS/OS-KBs/What_is_the_difference_between_Self-Signed_and_CA-Signed_Certificates" rel="noopener noreferrer"&gt;ONTAP management protocols and the TLS handshake&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/fsx-ontap-generate-certs.html" rel="noopener noreferrer"&gt;generating a self-signed certificate for FSx for ONTAP&lt;/a&gt;), so by default a certificate-verifying call fails. This failure must be classified as a TLS-trust failure, not "unreachable." Python's urllib wraps the SSL error inside the URLError reason, so you unwrap the reason before classifying. The option to disable certificate validation is used only as an explicitly recorded diagnostic, never as a silent bypass. In production, install a certificate signed by a trusted CA on ONTAP and have the client trust that CA.&lt;/p&gt;

&lt;p&gt;The Accept header needs care too. An ONTAP REST API listing does not accept &lt;code&gt;multipart/form-data&lt;/code&gt; (HTTP 406, ONTAP error code 262269). A directory listing must use Accept &lt;code&gt;application/json&lt;/code&gt;, and a byte read branches to multipart with a length.&lt;/p&gt;

&lt;p&gt;There is also a finding on FSx for ONTAP role scope. At the file-system level the roles are fixed (fsxadmin, fsxadmin-readonly); custom roles are definable only at the SVM level. An SVM-scoped user gets HTTP 401 on the management LIF (there is no SVM REST management LIF). The read-only role passes all GETs, and a write returns HTTP 403 / ONTAP error code 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path D (EKS and Trident): using FSx for ONTAP as a container's persistent volume
&lt;/h2&gt;

&lt;p&gt;Path D is not the same job. I tested, in the Container-Datastore repository, a path in which containers on EKS use FSx for ONTAP volumes as persistent volumes through Trident. Unlike A through C-REST, it does not read the data of an existing volume: Trident provisions new volumes and LUNs for the containers.&lt;/p&gt;

&lt;p&gt;What I confirmed is whether data written by a container remains after a pod restart and after a reschedule to another node. I created two StorageClasses. &lt;code&gt;ontap-san&lt;/code&gt; is iSCSI, with RWO, which only one pod writes to. &lt;code&gt;ontap-nas&lt;/code&gt; is NFS, with RWX, which several pods share. I provisioned one 1 GiB PVC from each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iSCSI / RWO&lt;/td&gt;
&lt;td&gt;PVC provisioning&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Bound&lt;/code&gt; in about 10 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iSCSI / RWO&lt;/td&gt;
&lt;td&gt;After writing a marker, after a pod restart (same node), and after cordoning the node and deleting the pod (other node, other AZ)&lt;/td&gt;
&lt;td&gt;The marker matched all three times. The new pod started only after the old pod had finished deleting. Both nodes mounted the same multipath device (ext4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFS / RWX&lt;/td&gt;
&lt;td&gt;PVC provisioning&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Bound&lt;/code&gt; in about 10 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFS / RWX&lt;/td&gt;
&lt;td&gt;Pod B on another node reads what pod A wrote&lt;/td&gt;
&lt;td&gt;Matched (NFS v4.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFS / RWX&lt;/td&gt;
&lt;td&gt;After restarting the Deployment so every pod was new&lt;/td&gt;
&lt;td&gt;Matched. An append made after the restart was readable from another pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the corrected template, no manual step was needed (the Trident node pods restarted 0 times and both nodes could use iSCSI) and the persistence results in the table held. I also checked importing an existing volume: I wrote a file to a volume created with the FSx API, then imported it with an annotated PVC. It bound in about 10 seconds and a pod read the pre-existing file. ONTAP renamed the volume to &lt;code&gt;trident_pvc_…&lt;/code&gt;, keeping its UUID and junction path. With reclaim policy &lt;code&gt;Retain&lt;/code&gt;, deleting the PVC and the PV left the ONTAP volume in place (though Trident kept its own record, so backend deletion waited until &lt;code&gt;tridentctl delete volume&lt;/code&gt; removed it). As a limit, for RWX I confirmed shared visibility, not consistency when several pods write the same file at once.&lt;/p&gt;

&lt;p&gt;I also measured backend failure shapes. A backend with a wrong credential turned &lt;code&gt;Failed&lt;/code&gt; (HTTP 401) and one pointed at an unreachable management LIF turned &lt;code&gt;Failed&lt;/code&gt; (no route to host); both left the PVC &lt;code&gt;Pending&lt;/code&gt; with a "no available backends" event. Changing a working backend's credential to a wrong value kept it running on the old credential until it was reloaded: existing pods' reads and writes did not stop, only new PVC provisioning did. This failure test locked the disposable SVM's &lt;code&gt;vsadmin&lt;/code&gt;, which also stopped another backend using the same user, so I unlocked it as the file system administrator. Failure tests are safer with a separate SVM user, or run after the other checks.&lt;/p&gt;

&lt;p&gt;During verification I found three defects in the CloudFormation template I used. I recorded them in an issue (&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Container-Datastore-Patterns/issues/26" rel="noopener noreferrer"&gt;#26&lt;/a&gt;) and fixed them in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Container-Datastore-Patterns/pull/27" rel="noopener noreferrer"&gt;#27&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The default Kubernetes version was 1.31, which was in extended support in ap-northeast-1 on the day I tested. I removed the default so the user chooses a version in standard support.&lt;/li&gt;
&lt;li&gt;The extra security group the template created attached to the control-plane network interfaces, not to the managed nodes (both nodes, one observation). I changed the source of the FSx-side ingress rules to the cluster security group, which the nodes carry.&lt;/li&gt;
&lt;li&gt;The Amazon Linux 2023 EKS AMI did not include the iSCSI tools. I made a launch template install them, and with the changed template I confirmed that first-boot nodes had the tools and that Trident's node pods detected iSCSI without a restart (two nodes, one observation).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teardown showed one more behavior. The FSx API (&lt;code&gt;describe-volumes&lt;/code&gt;) reflects the volumes Trident creates on the ONTAP side with a delay. It did not list them at 23 seconds and at 11 minutes after creation, and listed them at 26 minutes. After I deleted the volumes on ONTAP, it still listed them at 41 minutes and no longer did at 44 minutes (one observation). While the FSx API showed a non-root volume, deleting the SVM was rejected. After teardown, check for leftovers with the ONTAP REST API as well as the FSx API. Deleting the PVCs removed the LUN and the volumes from ONTAP, and the file system's backup count was the same before and after (environments with automatic backups enabled are unchecked).&lt;/p&gt;

&lt;p&gt;The results and the procedure are in the Container-Datastore record (&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Container-Datastore-Patterns/blob/24b411dbec9d4f7136f688bde3120fa18c548e9c/docs/en/atx-containerization-fsxn-storage-verification.md" rel="noopener noreferrer"&gt;section 4.3 of the storage-verification memo&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Path E (services that read through S3 Access Points): analytics and RAG
&lt;/h2&gt;

&lt;p&gt;Path E is AWS services that read FSx for ONTAP data through the S3 API. It goes through the same S3 Access Points as candidate C, but the caller is not a Lambda I wrote; it is a service such as Athena or Bedrock Knowledge Bases. I tested analytics (E-1) in the Lakehouse repository and RAG (E-2) in the Agentic-RAG repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  E-1 Analytics: Athena reads Parquet
&lt;/h3&gt;

&lt;p&gt;I put a 10,000-row Parquet file (Snappy compression, about 150 KB, microsecond timestamps) on a disposable volume, and read a table registered in the Glue Data Catalog with Athena (engine v3) through an internet-origin access point (UNIX user).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PutObject, ListObjectsV2, and HeadObject on the access point&lt;/td&gt;
&lt;td&gt;All succeeded. The listing showed storage class &lt;code&gt;FSX_ONTAP&lt;/code&gt; and size 150,902 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT COUNT(*)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,000 rows, matching the source row count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conditional aggregation (&lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The counts (1,667, 1,667, 1,666) and totals for the three regions matched the values computed from the source data exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run time (small data, cache-resident)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COUNT&lt;/code&gt; took 821 ms in total and scanned 0 bytes. The aggregation took 942 ms in total and scanned 12,384 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The run times are dominated by planning on small data. They are not production-scale values.&lt;/p&gt;

&lt;p&gt;Two premises apply. Athena needed an internet-origin access point; a VPC-origin one did not work. The SVM I used was joined to Active Directory. The test succeeded, but I did not measure behavior when the domain controller is unreachable. Query results are written to a separate S3 bucket, not to FSx. I did not retrieve the ONTAP version. Other engines (Glue, EMR Serverless, Redshift Spectrum, and so on) are in other records in the Lakehouse repository, and I did not re-measure them here.&lt;/p&gt;

&lt;p&gt;The record is the Lakehouse &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Lakehouse-Integrations/blob/a0d2cc1dd4a959c69eb2c1a12f144f585d4005cd/verification-pack/athena-parquet-read/evidence/2026-10-28/evidence-record.yaml" rel="noopener noreferrer"&gt;evidence-record.yaml&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  E-2 RAG: narrowing search results by permission
&lt;/h3&gt;

&lt;p&gt;I took documents on a disposable volume (on an SVM not joined to Active Directory), ingested them from an internet-origin access point into a Bedrock Knowledge Base (S3 Vectors as the vector store), and checked on a deployed environment a path in which a filter inside the web app matches SIDs that permit each document and narrows the search results. I measured four cases.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Principal and search&lt;/th&gt;
&lt;th&gt;Retrieved&lt;/th&gt;
&lt;th&gt;Allowed&lt;/th&gt;
&lt;th&gt;Denied&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;An administrator (holding the SIDs of the administrator user, Domain Admins, and Everyone) searches "company sales"&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;SID match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;A general user (the individual user's SID and Everyone only) runs the same query as case 1&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;SID match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A user with no registered SID runs the same query as case 1&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Deny all (fail-closed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;The administrator searches "product overview" (over Everyone documents)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;SID match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;The general user runs the same query&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;SID match&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In cases 2 and 3, the same query as case 1 retrieved 6 documents, and all 6 were denied. That shows the filter removed the documents after retrieval, not that the user's forbidden documents were never found. For the user with no registered SID, all 6 retrieved documents were denied, so the path was fail-closed (deny everything when there is no permission information).&lt;/p&gt;

&lt;p&gt;The four cases above were measured on the inline path (filter inside the web app), which is the production default. That path has no cache and reads DynamoDB each time, so adding the confidential group's SID to a general user made the same query ALLOW immediately, and removing it returned it to DENY. As an alternative, with the filter moved to a Lambda (&lt;code&gt;usePermissionFilterLambda=true&lt;/code&gt;) I measured the same four cases plus group add/remove, confirmed &lt;code&gt;filterLog.source&lt;/code&gt; reads &lt;code&gt;lambda&lt;/code&gt;, and got the same verdicts. The Lambda path has a five-minute cache: after removing the group, a cached entry returned the old verdict until it was cleared.&lt;/p&gt;

&lt;p&gt;A CDK deploy plus one setup script get ingestion (11 documents) and the four cases through with no manual step, except one: creating the Cognito users, whose password does not meet the pool policy. Code defects found during the deploy are recorded in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Agentic-Access-Aware-RAG/issues/228" rel="noopener noreferrer"&gt;#228&lt;/a&gt; and fixed. The set was 11 documents, and each case is one observation. I did not measure an SVM joined to Active Directory.&lt;/p&gt;

&lt;p&gt;The record is &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Agentic-Access-Aware-RAG/blob/3dd74b5ddea63f05aef1790b8100dff4975abdfb/docs/en/evaluation.md" rel="noopener noreferrer"&gt;section 3.3 of the Agentic-RAG evaluation.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How failures return per path, and what was confirmed for D and E
&lt;/h2&gt;

&lt;p&gt;For A, B, C, and C-REST, which ran the same job, authentication, network, authorization, and the way failures return changed as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;A (.NET / NFS)&lt;/th&gt;
&lt;th&gt;B (Java / NFS)&lt;/th&gt;
&lt;th&gt;C (Python / S3 Access Points)&lt;/th&gt;
&lt;th&gt;C-REST (Python / ONTAP REST API)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth principal&lt;/td&gt;
&lt;td&gt;OS (uid, AUTH_SYS)&lt;/td&gt;
&lt;td&gt;OS (uid, AUTH_SYS)&lt;/td&gt;
&lt;td&gt;AWS IAM&lt;/td&gt;
&lt;td&gt;ONTAP credential (basic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization principal&lt;/td&gt;
&lt;td&gt;OS (NFS permissions)&lt;/td&gt;
&lt;td&gt;OS (NFS permissions)&lt;/td&gt;
&lt;td&gt;AWS IAM + access-point policy&lt;/td&gt;
&lt;td&gt;ONTAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;NFS mount (in-VPC)&lt;/td&gt;
&lt;td&gt;NFS mount (in-VPC)&lt;/td&gt;
&lt;td&gt;public S3 endpoint (outside the VPC)&lt;/td&gt;
&lt;td&gt;management LIF (in-VPC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth failure returns&lt;/td&gt;
&lt;td&gt;at the NFS mount stage&lt;/td&gt;
&lt;td&gt;at the NFS mount stage&lt;/td&gt;
&lt;td&gt;(credential is IAM-side)&lt;/td&gt;
&lt;td&gt;HTTP 401 / ONTAP 6691623&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization deny returns&lt;/td&gt;
&lt;td&gt;NFS permission error&lt;/td&gt;
&lt;td&gt;NFS permission error&lt;/td&gt;
&lt;td&gt;AccessDenied / HTTP 403&lt;/td&gt;
&lt;td&gt;HTTP 403 / ONTAP code 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreachable returns&lt;/td&gt;
&lt;td&gt;mount failure&lt;/td&gt;
&lt;td&gt;mount failure&lt;/td&gt;
&lt;td&gt;(public endpoint)&lt;/td&gt;
&lt;td&gt;connection timeout → unreachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid identifier returns&lt;/td&gt;
&lt;td&gt;file not found&lt;/td&gt;
&lt;td&gt;file not found&lt;/td&gt;
&lt;td&gt;(per key)&lt;/td&gt;
&lt;td&gt;HTTP 404 / ONTAP 6684674&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS consideration&lt;/td&gt;
&lt;td&gt;none (NFS)&lt;/td&gt;
&lt;td&gt;none (NFS)&lt;/td&gt;
&lt;td&gt;none (AWS terminates TLS; no client-side certificate)&lt;/td&gt;
&lt;td&gt;ONTAP self-signed certificate → must classify as trust failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency on AD DC reachability&lt;/td&gt;
&lt;td&gt;none (measured on an SVM not joined to AD)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same "deny" returns as an OS permission error over NFS, as &lt;code&gt;AccessDenied&lt;/code&gt; / HTTP 403 over S3 Access Points, and as HTTP 403 with an ONTAP error code over ONTAP REST API. When you implement failure classification, you have to fold in this per-path difference.&lt;/p&gt;

&lt;p&gt;D and E are different workloads, and I did not measure their failure cases in the same form. I separate what I confirmed from what I did not into another table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What I confirmed&lt;/th&gt;
&lt;th&gt;What I did not confirm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D (Trident PVs)&lt;/td&gt;
&lt;td&gt;iSCSI and NFS volumes were provisioned in about 10 seconds, and the data remained after a pod restart and a reschedule to another node. Deleting the PVCs removed the LUN and the volumes. An existing volume was imported as a PV and its pre-existing data was readable. A backend with a wrong credential or an unreachable management LIF returned &lt;code&gt;Failed&lt;/code&gt;, leaving the PVC &lt;code&gt;Pending&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Multi-AZ switchover. Performance. Same-file concurrent-write consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-1 (Athena)&lt;/td&gt;
&lt;td&gt;Reading and aggregating Parquet through S3 Access Points matched the source data. It succeeded on an SVM joined to AD&lt;/td&gt;
&lt;td&gt;Behavior when the domain controller is unreachable. Production-scale data. Write-back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-2 (Bedrock Knowledge Bases)&lt;/td&gt;
&lt;td&gt;Documents outside the user's permissions were removed from the search results, and a user with no permission information was denied everything. A deny is not an error; it returns as removal from the results. Group add/remove was reflected in the results. The inline and Lambda paths gave the same verdicts. CDK and one script nearly completed (only a password step by hand)&lt;/td&gt;
&lt;td&gt;An SVM joined to Active Directory. Response time. Natural expiry of the Lambda cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to choose a path
&lt;/h2&gt;

&lt;p&gt;Not by winner, but by the client you use and where you want authorization to happen. I state trade-offs symmetrically, including for the option I lean toward.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Trade-off / consideration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NFS mount (A, B)&lt;/td&gt;
&lt;td&gt;When a Linux server process needs POSIX file semantics (such as exclusive locking) directly&lt;/td&gt;
&lt;td&gt;Auth is uid-based AUTH_SYS and depends on OS permissions. It assumes an in-VPC mount and is hard to use directly from serverless. File-lock behavior is only observable with two processes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own code calling S3 Access Points (C)&lt;/td&gt;
&lt;td&gt;When an S3 API client such as Lambda should be authorized by AWS IAM&lt;/td&gt;
&lt;td&gt;Authorization is IAM plus the access-point policy; it holds no ONTAP credential. It suits jobs that fit the S3 API operation set (it does not express POSIX semantics such as exclusive locking). The network origin is fixed at creation. It uses the public S3 endpoint, so an in-VPC Lambda is not required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP REST API (C-REST)&lt;/td&gt;
&lt;td&gt;When you want fine control with an ONTAP credential and ONTAP-side authorization for management or file operations&lt;/td&gt;
&lt;td&gt;The management LIF is reachable only from inside the VPC, so you need an in-VPC Lambda (mind cold starts and the ENI). You must handle the self-signed certificate (provide a trusted CA in production; disable verification only as an explicitly recorded diagnostic). File-system-level roles are fixed; custom roles are SVM-level only. Large files hit the 1 MB-per-call limit, so split them across calls.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trident persistent volumes (D)&lt;/td&gt;
&lt;td&gt;When stateful containers on EKS should use FSx for ONTAP volumes as PVs; also when you want to import existing NFS/SMB data as a PV&lt;/td&gt;
&lt;td&gt;You need EC2 worker nodes (it does not run on Fargate), and iSCSI needs the tools and a multipath configuration on the nodes. Node preparation has no guaranteed start order, so restart the DaemonSet if the order breaks. The source of the FSx SG ingress is the cluster SG, not the node SG. Trident's objects are applied with kubectl, outside CloudFormation. The EKS cluster is billed. I confirmed both provisioning new volumes and importing an existing one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An AWS service that takes S3 as input (E)&lt;/td&gt;
&lt;td&gt;When a service such as Athena or Bedrock Knowledge Bases should use data on FSx for ONTAP&lt;/td&gt;
&lt;td&gt;You are bound to the access-point settings (Athena needs an internet-origin one). Choose internet-origin when the service comes from outside the VPC, VPC-origin when it stays inside. Per-document permissions must be designed outside the service (for example, a filter in the app). It does not express all of S3 (for example, conditional writes are unavailable).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of them are paths to the same FSx for ONTAP data. The basis for choosing is what the client is (a Linux process, an S3 API client, something holding an ONTAP credential, a Kubernetes container, or an AWS managed service) and where you want authorization to happen (the OS, IAM, ONTAP, or the app).&lt;/p&gt;

&lt;p&gt;Reaching one volume through several paths at once (NFS, SMB, S3 Access Points) shares that volume's throughput. When you design for several paths at the same time, size the combined load against the volume's throughput.&lt;/p&gt;

&lt;p&gt;This table does not decide which path is better. It lays out a fact: authentication, authorization, and the shape of failures swap per path. Which path fits your system, and whether you can keep operating it, is for someone who knows the app and the data to judge from the facts laid out here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and the path options
&lt;/h2&gt;

&lt;p&gt;The result is the three findings from the start.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same file-processing job ran as four implementations (.NET, Java, and Python) against disposable volumes on the same FSx for ONTAP file system, without moving the data. Changing the path swapped the authentication principal, the network boundary, the authorization principal, and how failures return.&lt;/li&gt;
&lt;li&gt;Containers on EKS read the data in volumes Trident provisioned after a pod restart and after a reschedule to another node (SAN and NAS, once each). Athena read Parquet through S3 Access Points and the aggregation matched the source data (10,000 rows). In Bedrock Knowledge Bases searches, documents the user was not permitted to see were removed from the results (four cases, 11 documents).&lt;/li&gt;
&lt;li&gt;The basis for choosing is the client you use and where you want authorization to happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All costs are estimates before billing, for disposable environments that lived minutes to a few hours, not production always-on costs. Each A through C-REST measurement was under $1 in an approved disposable environment. Path D's stack lived about 36 to 70 minutes; from the unit prices (ap-northeast-1, Pricing API, 2026-10) of an EKS cluster at $0.10/hour plus two nodes at $0.124/hour each, that is about $0.2–0.4, excluding NAT data processing. Path E-2 is a WAF Web ACL ($5/month, $1/month per rule, prorated) plus small usage-based charges for CloudFront, Lambda, and Bedrock, on the order of under $3 for a disposable few hours. For E-1 and E-2 I did not compute a cost for the actual time the resources lived, and I did not check the billed amounts.&lt;/p&gt;

&lt;p&gt;Each case was measured once, and I said on the spot where I got through by hand and where I did not retrieve a value. Path E-2 is nearly completed by CDK and one script (only the password step is by hand), and the SAN and NAS data persistence on path D was observed on the corrected template with no manual step.&lt;/p&gt;

&lt;p&gt;Repository:&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns" rel="noopener noreferrer"&gt;https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The previous article is "&lt;a href="https://dev.to/aws-builders/keeping-the-data-where-it-is-modernizing-only-the-app-from-net-framework-to-lambda-what-changes-1p4o"&gt;Keeping the Data Where It Is, Modernizing Only the App From .NET Framework to Lambda — What Changes and What Does Not (A 1-Environment Test)&lt;/a&gt;."&lt;/p&gt;

&lt;p&gt;To try this in your own environment, start with the premises in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/main/docs/en/reproduce.md" rel="noopener noreferrer"&gt;reproduce.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Modernization does not have to replace everything at once. With the data in one place, you can add access paths a little at a time — first an EC2 instance over NFS, then Lambda, and later EKS or analytics and AI. You choose each path to fit the runtimes your team can operate and where you want authorization to happen. Choosing FSx for ONTAP as the destination lets you take this "keep the data in place, add access paths in stages" approach in combination with the various AWS services. This article measured those six paths actually reaching the same data, and how authentication, authorization, and the shape of failures change per path.&lt;/p&gt;

&lt;p&gt;Whether you are about to modernize on AWS or already use FSx for ONTAP, I hope this gives you something to decide with as you go step by step.&lt;/p&gt;

&lt;p&gt;See you next time.&lt;/p&gt;

&lt;p&gt;Bye now!!&lt;/p&gt;

&lt;p&gt;Yoshiki Fujiwara&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/42" rel="noopener noreferrer"&gt;Candidate A runtime verification issue (#42)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/46" rel="noopener noreferrer"&gt;Candidate B spec / runtime issue (#46)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/47" rel="noopener noreferrer"&gt;Candidate C spec / runtime issue (#47)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/48" rel="noopener noreferrer"&gt;Candidate C-REST spec / runtime issue (#48)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/38" rel="noopener noreferrer"&gt;Runtime verification tracking (#38)&lt;/a&gt;. A, B, C, and C-REST are complete; their deliverables are in the issues and records above. #38 now tracks the remaining deep-dives such as performance and scale. The letters A to C in #38 differ from this article's B, C, and C-REST; the mapping is in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/issues/45" rel="noopener noreferrer"&gt;#45&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Container-Datastore-Patterns/blob/24b411dbec9d4f7136f688bde3120fa18c548e9c/docs/en/atx-containerization-fsxn-storage-verification.md" rel="noopener noreferrer"&gt;Path D record (Container-Datastore, section 4.3 of the storage-verification memo)&lt;/a&gt;, and the issue for the template defects (&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Container-Datastore-Patterns/issues/26" rel="noopener noreferrer"&gt;#26&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Lakehouse-Integrations/blob/a0d2cc1dd4a959c69eb2c1a12f144f585d4005cd/verification-pack/athena-parquet-read/evidence/2026-10-28/evidence-record.yaml" rel="noopener noreferrer"&gt;Path E-1 record (Lakehouse, evidence-record.yaml)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Agentic-Access-Aware-RAG/blob/3dd74b5ddea63f05aef1790b8100dff4975abdfb/docs/en/evaluation.md" rel="noopener noreferrer"&gt;Path E-2 record (Agentic-RAG, section 3.3 of evaluation.md)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/access-points-for-fsxn.html" rel="noopener noreferrer"&gt;FSx for ONTAP S3 Access Points user guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/fsx-ontap.html" rel="noopener noreferrer"&gt;Amazon EKS User Guide: use FSx for NetApp ONTAP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/porting/" rel="noopener noreferrer"&gt;Microsoft Learn: Upgrade .NET apps overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.netapp.com/us-en/ontap-automation/reference/api_reference.html" rel="noopener noreferrer"&gt;ONTAP REST API reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>kubernetes</category>
      <category>rag</category>
    </item>
    <item>
      <title>Keeping the Data Where It Is, Modernizing Only the App From .NET Framework to Lambda — What Changes and What Does Not (A 1-Environment Test)</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:48:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/keeping-the-data-where-it-is-modernizing-only-the-app-from-net-framework-to-lambda-what-changes-1p4o</link>
      <guid>https://dev.to/aws-builders/keeping-the-data-where-it-is-modernizing-only-the-app-from-net-framework-to-lambda-what-changes-1p4o</guid>
      <description>&lt;h2&gt;
  
  
  Keeping the data you already have, modernizing only the app — what changes across SMB, NFS, and S3 Access Points
&lt;/h2&gt;

&lt;p&gt;Business apps written for .NET Framework are still running in many Windows environments. .NET Framework runs only on Windows, and its feature set is effectively frozen. Current .NET (modern .NET) is cross-platform: it runs on Linux, in containers, and on serverless such as AWS Lambda. Moving an app to modern .NET frees it from the OS dependency and the support-window constraint, and lets it run without an always-on server, paying only for what it uses. Microsoft itself frames the move from .NET Framework to modern .NET as a good opportunity to containerize and go cloud-native (&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/porting/" rel="noopener noreferrer"&gt;Microsoft Learn: Upgrade .NET apps overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;But work usually stalls on the same question: &lt;strong&gt;the app can move, but what about the data the app has been reading and writing?&lt;/strong&gt; Do I rebuild that data or relocate it, and what happens to its permissions and protocols?&lt;/p&gt;

&lt;p&gt;No new announcement from AWS or any other vendor prompted this work. I have been working through modernization on AWS as a series of themes — migration, multiprotocol access, performance, S3 Access Points, a serverless file portal, a data lake, observability — and the step I had not yet taken was migrating the application code itself. Then AWS's AI-Driven Modernization published its &lt;a href="https://github.com/aws-samples/sample-ai-modernization-flow" rel="noopener noreferrer"&gt;AI Modernization Flow&lt;/a&gt; (a workflow that has an AI agent carry out a disciplined migration), and I used it to take exactly that step.&lt;/p&gt;

&lt;p&gt;This post is a measurement record of what changed and what did not when I moved &lt;strong&gt;only the app, from .NET Framework 4.8 to current .NET and AWS Lambda in four stages, with the data left on a single volume&lt;/strong&gt;. Across the stages I added paths for opening that same data: SMB from a Windows Amazon EC2 instance, NFS and cifs (a way to use SMB from Linux) from a Linux Amazon EC2 instance, and S3 Access Points (a way to open the volume through the S3 API) from AWS Lambda. The data lives on a single volume of Amazon FSx for NetApp ONTAP (FSx for ONTAP from here on; NetApp's ONTAP offered as a managed AWS storage service), and I did not change that volume from start to finish.&lt;/p&gt;

&lt;p&gt;First, the caveats. I measured one app in one environment (ONTAP 9.19.1P3, Single-AZ, ap-northeast-1), and each measured value in this post comes from one to a few runs. A value I saw only once is labeled "one observation". No value is a guarantee that production will behave the same way. Costs are the values AWS Cost Explorer returned before billing is final (Estimated), not invoice amounts. When a later chapter says "as stated up front", it refers to this paragraph: one app, one environment, one to a few runs, and Estimated costs.&lt;/p&gt;

&lt;p&gt;The one app I measured is DocIntake, a sample app I wrote for this verification. It is not a customer app and not a production app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Role: the Worker (the app proper, one of the two console apps) reads the documents in an inbox folder on the volume (&lt;code&gt;seed/inbox/&lt;/code&gt;), processes them, and writes a report of the results (reports) and an index, &lt;code&gt;index.json&lt;/code&gt;. It is a batch-style app&lt;/li&gt;
&lt;li&gt;Structure: two console apps (the Worker, and the Probe that measures the five behaviors). It has no screen (UI), no Web layer, no database, and no authentication&lt;/li&gt;
&lt;li&gt;How it reaches storage: all file reads and writes go through &lt;code&gt;IFileStore&lt;/code&gt;, which has 5 methods. In the .NET Framework source, a single &lt;code&gt;Store&lt;/code&gt; key in appSettings selects the storage implementation (SMB in stage 0, the first of the four stages). As the stages advanced, I added NFS and S3 Access Points implementations&lt;/li&gt;
&lt;li&gt;Why it was built this way, and its size: so that the difference between paths can be observed behavior by behavior, I placed one habit that does not show up on Windows + SMB but can show up on another path in the source code for each of the five behaviors (the five behaviors are listed in the Probe row under "Terms used in this post"). It is .NET Framework 4.8, C#, 0 NuGet dependencies, 10 .cs files, and 935 lines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because this is the result of a single self-written console app, it does not necessarily apply to real business apps, or to apps that have a screen.&lt;/p&gt;

&lt;p&gt;You are probably wondering about these three points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can I move only the app to Lambda without moving the data?&lt;/li&gt;
&lt;li&gt;When I change the path that opens the data from SMB to NFS to S3 Access Points, what changes in how the same files behave?&lt;/li&gt;
&lt;li&gt;Which should I use to convert the code: atx (AWS's code analysis and conversion tool) or AIMF (the AI Modernization Flow mentioned above)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are the three findings, answering the three questions above in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Question 1 (can I move only the app to Lambda without moving the data?): yes. I converted the code of DocIntake and ran it on Lambda, while the data stayed in one FSx for ONTAP volume, appdata. In this post, "move" means converting the code and changing where the code runs and which path it uses to open the volume. It does not include copying or moving the data.

&lt;ul&gt;
&lt;li&gt;What did not change (where the data lives): the data stayed in the volume appdata from stage 0 through stage 3. I did not copy, move, or recreate that volume. The UUID of appdata, its security style (the method it uses to decide permissions; ntfs to the end, which decides permissions with NTFS ACLs), and the sha256 of the seed (the 6 files I placed at the start of the verification) did not change; I recorded and compared them at each stage boundary (these records are from the second of the two builds of the verification environment). Those 3 items are what I confirmed did not change. The output that the app writes did accumulate on the volume, and in stage 1 I changed the export policy and name mapping settings for NFS&lt;/li&gt;
&lt;li&gt;What did change (the code, and how it runs): three things changed: the app's code, where the code runs, and the path used to open the volume. I converted the code from .NET Framework 4.8 to .NET 10 (with atx, a code analysis and conversion tool, and AIMF, a kiro-cli migration workflow; the third finding covers how they differ). I changed where the code runs from a Windows EC2 instance to a Linux EC2 instance to Lambda. For the path, I added NFS and cifs to SMB, and finally added S3 Access Points&lt;/li&gt;
&lt;li&gt;What I checked: in stage 3, the reports and &lt;code&gt;index.json&lt;/code&gt; that the app itself (the Worker) wrote on Lambda through S3 Access Points were visible from the Linux client that opens the same volume over NFS (one observation)&lt;/li&gt;
&lt;li&gt;The code that ran at each stage: the converted code ran in stage 2 and stage 3. Stage 0 is the starting point with the unconverted code, and stage 1 adds a path without changing the code, so neither has converted code. The table below shows each stage&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Code that ran&lt;/th&gt;
&lt;th&gt;Where it ran&lt;/th&gt;
&lt;th&gt;Path to the volume&lt;/th&gt;
&lt;th&gt;Evidence that it ran&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;The unconverted Probe (.NET Framework 4.8). The counterpart on the Linux side is a Python version of the Probe (not DocIntake's code, but a separate script that measures the same 5 behaviors). The unconverted Worker ran once, later, as the stage 0 baseline&lt;/td&gt;
&lt;td&gt;A Windows EC2 instance and a Linux EC2 instance&lt;/td&gt;
&lt;td&gt;SMB from Windows; cifs from Linux&lt;/td&gt;
&lt;td&gt;The Probe records show every side and every pair as Success. The Worker exited with 0 and wrote reports and &lt;code&gt;index.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The same Probe as in stage 0 (no record of a code change). A Python version of the Probe on the Linux side. No record that the Worker ran&lt;/td&gt;
&lt;td&gt;A Windows EC2 instance and a Linux EC2 instance&lt;/td&gt;
&lt;td&gt;SMB from Windows; cifs and NFS from Linux&lt;/td&gt;
&lt;td&gt;The Probe records show every side and every pair as Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;The converted Worker and Probe (.NET 10). The counterpart on the Windows side is the unconverted Probe&lt;/td&gt;
&lt;td&gt;A Linux EC2 instance&lt;/td&gt;
&lt;td&gt;NFS for the Worker; NFS and cifs for the Probe&lt;/td&gt;
&lt;td&gt;The Worker exited with 0 and wrote reports and &lt;code&gt;index.json&lt;/code&gt;. Every Probe record is measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The converted Worker and Probe (.NET 10; two modes of one Lambda function)&lt;/td&gt;
&lt;td&gt;Lambda (inside the VPC)&lt;/td&gt;
&lt;td&gt;S3 Access Points&lt;/td&gt;
&lt;td&gt;The Worker exited with 0, and the output it wrote was visible from the NFS side (names and sizes only; I did not read the contents; one observation)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Question 2 (when I change the path that opens the data, what changes in how the same files behave?): some of how the same files behave changed with the path that opens them.

&lt;ul&gt;
&lt;li&gt;Case sensitivity: for each path, I measured whether a reference that points at an existing file with different letter case resolves to that file on the same volume, appdata. Both SMB paths (Windows, and a Linux cifs mount, which uses SMB from Linux) were case-insensitive (the reference resolved), while NFS and S3 Access Points were case-sensitive (it did not)&lt;/li&gt;
&lt;li&gt;Write visibility (the time until a file written by one client becomes visible to the other client): I measured both directions between Windows and Linux. When Windows wrote and Linux read, it took under 100 ms (35 ms when Linux read over NFS and 66 ms over cifs; the reading client tries every 100 ms to read the content the writer wrote, so these two values cannot be told apart). In the opposite direction, when Linux wrote and Windows read, it took about 8 seconds (7,968 to 8,342 ms: 7,968 ms when cifs wrote and 8,342 ms when NFS wrote). The asymmetry, fast when Windows writes and slow when Linux writes, had the same shape in three measurements (record identifiers s0, s1 and s2m). The values 35, 66, 7,968 and 8,342 ms above come from one of them, the single s2m measurement&lt;/li&gt;
&lt;li&gt;Other behaviors: the handling of names that contain &lt;code&gt;\&lt;/code&gt;, whether an exclusive lock is possible (a way of opening a file so that no one else can open it), and the writability check (whether writability is judged by a pre-evaluation of the ACL or by actually writing) also differ by path (see "Behavior by access path")&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Question 3 (which should I use, atx or AIMF?): atx and AIMF covered different scopes. I do not rank them.

&lt;ul&gt;
&lt;li&gt;What I compared: atx and AIMF are two different tools. I set side by side what each produced from processing the same app, DocIntake, once each&lt;/li&gt;
&lt;li&gt;atx (AWS Transform custom, AWS's code analysis and conversion tool): analyzed the code and converted it to .NET 10, and the net10.0 build of the converted code (on macOS) succeeded. But the five legacy-code habits that I had placed in the sample app, one for each of its five behaviors, and that assume Windows and SMB, remained in the converted code. The NFS and S3 Access Points implementations were not in atx's output&lt;/li&gt;
&lt;li&gt;AIMF (a workflow run with kiro-cli): took the atx analysis as input, produced ADRs (architecture decision records) and a migration ledger (a list of where each legacy-code habit was handled in the new code), and went on to implement NFS and S3 Access Points&lt;/li&gt;
&lt;li&gt;Limits of the comparison: one app, processed once with each tool, and the cost units differ (agent minutes for atx, Kiro credits for AIMF), so I did not compare which took less time or cost less&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you should take back to your own environment: each time you add a path that opens the data (SMB, NFS, S3 Access Points, and so on), measure the five behaviors again on the added path, together with how root is treated. The five behaviors are case sensitivity, handling of names that contain &lt;code&gt;\&lt;/code&gt;, exclusive locking, the writability check, and write visibility (listed in the Probe row under "Terms used in this post"). For root, an example is that on NFS the root user could not enter &lt;code&gt;/mnt/appdata&lt;/code&gt; (the cause is an inference from the export policy setting; see the stages 0 and 1 chapter and the stage 3 chapter).&lt;/p&gt;

&lt;p&gt;On cost: the total I could identify for the AWS resources used in this verification is $35.91. The breakdown is $27.49 that I could narrow down in Cost Explorer by tag; $4.80 for AWS Directory Service (Active Directory) in the second of the two builds of the verification environment (the first on 2026-10-07, the second on 2026-10-11 to 2026-10-12), which I could not narrow down and estimated by subtracting the steady usage of days without the verification from the usage of the verification days; and $3.62 for AWS Transform (atx) usage. All three are Estimated, not invoice amounts. The AWS Transform usage carries a Credit of the same amount, −$3.62, and the $35.91 does not deduct that Credit.&lt;/p&gt;

&lt;h3&gt;
  
  
  When this post fits and when it does not
&lt;/h3&gt;

&lt;p&gt;This post fits SAs, application developers, storage administrators, and Partner/SI engineers who are considering moving a .NET Framework app, built around a Windows shared folder (SMB), to Linux or Lambda without moving its data. It reports one such app, opened over SMB, NFS, and S3 Access Points against one FSx for ONTAP volume (ntfs, Single-AZ), so it is useful for judging cases with the same conditions (an SMB-based .NET Framework app, an ntfs volume, Single-AZ).&lt;/p&gt;

&lt;p&gt;This post does not fit if you need any of the following, because I either did not measure it or measured it under different conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance (this verification put it out of scope, so I did not measure it; "What this post covers and what it does not" points to results that other projects measured)&lt;/li&gt;
&lt;li&gt;Behavior on a volume whose security style is &lt;code&gt;unix&lt;/code&gt; or &lt;code&gt;mixed&lt;/code&gt; (I measured only ntfs)&lt;/li&gt;
&lt;li&gt;Migrating the screens or Web layer of an app that has a UI (the app I measured is a console app with no screen and no Web layer)&lt;/li&gt;
&lt;li&gt;Behavior on Multi-AZ (I built only Single-AZ)&lt;/li&gt;
&lt;li&gt;A guarantee for production (every value is from one to a few runs in one environment)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reading by purpose
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You only want the conclusions: the three findings in this chapter are enough.&lt;/li&gt;
&lt;li&gt;You want to know whether changing the path causes trouble: "Behavior by access path" and "Summary and choosing an access path"&lt;/li&gt;
&lt;li&gt;You are choosing between atx and AIMF: "Choosing between atx and AIMF"&lt;/li&gt;
&lt;li&gt;You want to know how to hand out a development environment without copying the data: the FlexClone subsection of "Staged design and the single-volume constraint" (it reports only what I read in documentation; I did not create a FlexClone in this verification)&lt;/li&gt;
&lt;li&gt;You want to know the cost: "Cost estimates and observations"&lt;/li&gt;
&lt;li&gt;You want the conditions and limits of the measurement, or want to reproduce it: "Test environment and method" and "What has not been measured yet"&lt;/li&gt;
&lt;li&gt;You want to read in order: the staged design → stages 0 and 1 → stage 2 → stage 3 → behavior by access path&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this post covers and what it does not
&lt;/h2&gt;

&lt;p&gt;This post reports five behaviors of one app, measured in one environment. It is not an exhaustive comparison.&lt;/p&gt;

&lt;p&gt;What this post covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It covers what changed and what stayed the same on the volume across four stages (SMB, adding NFS, new .NET, Lambda). The data did not move and only the path changed, so differences can be traced to the path&lt;/li&gt;
&lt;li&gt;It covers how I checked the measurements, and failures written up with numbers. Measuring without a positive control (a control that looks for something that must be found) invalidated the case-sensitivity values in 6 measurement runs. The same kind of error could occur in other measurements&lt;/li&gt;
&lt;li&gt;It covers how to choose between atx and AIMF. I have one pair of results where both processed the same app, so the difference in scope can be compared concretely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this post does not cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not cover volumes whose security style is &lt;code&gt;unix&lt;/code&gt; or &lt;code&gt;mixed&lt;/code&gt;. I decided to keep the security style at ntfs, and I did not create volumes with the other styles&lt;/li&gt;
&lt;li&gt;It does not cover the container migration branch. The documents in this verification's repository hold only an explanation of the branch point and a link to another repository, and I did not go down the branch. The other repository that covers the branch (&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Container-Datastore-Patterns/blob/2686fee31f984910689f6535b15626e55d5d2f0e/README.md" rel="noopener noreferrer"&gt;Container-Datastore Patterns&lt;/a&gt;, Japanese) is also, so far, at literature review and static validation (cfn-lint), and lists deployment to a real environment as the next stage&lt;/li&gt;
&lt;li&gt;It does not cover apps that have a UI (a Web layer such as ASP.NET, or a desktop app), or tests that operate a screen. DocIntake, the sample app in this verification, is a console app with no screen and no Web layer, and I ran no screen-operation tests&lt;/li&gt;
&lt;li&gt;It does not cover creating a FlexClone in a real environment, or the five behaviors on the clones. I did not create a single FlexClone in this verification, and the creation script went only as far as a dry run. The "Staged design and the single-volume constraint" chapter reports what I read in the documentation about FlexClone (documented)&lt;/li&gt;
&lt;li&gt;It does not cover other ONTAP versions or Multi-AZ. The only configuration I built was Single-AZ on ONTAP 9.19.1P3&lt;/li&gt;
&lt;li&gt;It does not cover reconciliation with an invoice. I have not checked an invoice, and the costs in this post are only the Cost Explorer values before billing is final (Estimated)&lt;/li&gt;
&lt;li&gt;It does not cover performance. This verification measured how behavior differs per path (the five behaviors). I did not run measurements with performance conditions decided in advance (large files, many files, long runs), and ADR-2, AIMF's record of design decisions, also put performance out of scope. Another project measured the performance of the S3 Access Points path in two cases. Both are carried-over (values measured in another verification of mine), and neither is a value measured in this post's environment. Neither document records the ONTAP version

&lt;ul&gt;
&lt;li&gt;From an Amazon EC2 instance (c5n.9xlarge) using Python, the median latency of a PUT to S3 Access Points at concurrency 1 was 363.7 ms for 1 MiB and 331.5 ms for 8 MiB. A fixed cost per request (about 330 ms) dominated over object size (2026-09-01, ap-northeast-1, Single-AZ, 128 MBps, NetworkOrigin VPC, UNIX root identity; &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/throughput-iops-concurrency.md" rel="noopener noreferrer"&gt;measurement of the S3 Access Points path performance&lt;/a&gt;, Japanese)&lt;/li&gt;
&lt;li&gt;From a Lambda function outside the VPC (1,769 MB), the median latency of a 1 MB GetObject against S3 Access Points at concurrency 10 was 73.2 ms (2026-05-25, ap-northeast-1, Single-AZ, 128 MBps, NetworkOrigin Internet; &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/blob/1961fef256873d70a0d60c71d5697fd69a012765/docs/s3ap-benchmark-results.md" rel="noopener noreferrer"&gt;S3 Access Points benchmark results&lt;/a&gt;, Japanese). That benchmark results document also lists the path this post uses, from a Lambda function in the VPC to an S3 Access Point with VPC origin, as not yet measured&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Terms used in this post
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;One-line meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ntfs / security style&lt;/td&gt;
&lt;td&gt;The security style is the method a volume uses to decide permissions, and ntfs decides with NTFS ACLs. This post uses ntfs to the end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMB&lt;/td&gt;
&lt;td&gt;The protocol Windows uses for file sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NFS&lt;/td&gt;
&lt;td&gt;The protocol Linux uses for file sharing. This post uses NFS 4.1 with &lt;code&gt;sec=sys&lt;/code&gt; (a simple authentication where the client claims its uid)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cifs mount&lt;/td&gt;
&lt;td&gt;A way to mount an SMB share from Linux. The path is SMB and the client is Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Access Points&lt;/td&gt;
&lt;td&gt;An endpoint that reads and writes an FSx for ONTAP volume through the S3 API. An S3 API client such as Lambda reaches the same volume without copying the data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NetworkOrigin&lt;/td&gt;
&lt;td&gt;The setting for where an S3 Access Point can be reached from (a VPC in this post). You can specify it only when the access point is created, and changing it means recreating the access point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;atx (AWS Transform custom)&lt;/td&gt;
&lt;td&gt;AWS's code analysis and conversion tool. I use two of its transformations: analysis (comprehensive-codebase-analysis) and conversion (dotnet-modernization)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AIMF&lt;/td&gt;
&lt;td&gt;Short for AI Modernization Flow. A workflow that runs through Phase 0a to 4 with kiro-cli and leaves ADRs and a ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADR / migration ledger&lt;/td&gt;
&lt;td&gt;An ADR is a record of a design decision and its reason. The ledger (AP-1 to 14) lists where each legacy-code habit was handled in the new code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe&lt;/td&gt;
&lt;td&gt;A console app that measures five behaviors (case sensitivity, names containing &lt;code&gt;\&lt;/code&gt;, exclusive locking, the writability check, write visibility) and records them as JSON. An exclusive lock is a way of opening a file so that no one else can open it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive control&lt;/td&gt;
&lt;td&gt;A control that looks for something that must be found, through the same path, to confirm the measurement works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;write-visibility&lt;/td&gt;
&lt;td&gt;The label the Probe records use for write visibility: the time until a file written by one client becomes visible to the other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.3 short name&lt;/td&gt;
&lt;td&gt;A short Windows-compatible alias given to a long file name (for example &lt;code&gt;PROBES~1.TXT&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name mapping&lt;/td&gt;
&lt;td&gt;An ONTAP setting that maps a Windows user to a UNIX user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexClone&lt;/td&gt;
&lt;td&gt;An ONTAP feature that creates a writable point-in-time copy, as a separate volume, from a volume's Snapshot (its state at one moment). I did not create one in this post&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The modernization work so far, and the new step this time
&lt;/h2&gt;

&lt;p&gt;This post continues an ongoing body of work on modernizing on AWS. So far I have worked through block storage migration with AWS Transform, multiprotocol access on FSx for ONTAP, hands-on throughput and block-performance verification, using S3 Access Points, a serverless file portal, data lake integration, and observability — each published as a GitHub repository and a blog post. This time the new, concrete step was migrating the application code itself.&lt;/p&gt;

&lt;p&gt;What prompted this work is AWS's "&lt;a href="https://aws.amazon.com/jp/blogs/news/aidm-introducing-ai-modernization-flow/" rel="noopener noreferrer"&gt;AI-Driven Modernization (AIDM)&lt;/a&gt;" and its &lt;a href="https://github.com/aws-samples/sample-ai-modernization-flow" rel="noopener noreferrer"&gt;AI Modernization Flow (AIMF)&lt;/a&gt;. I ran this migration with AIMF. AIMF is a workflow that has an AI agent carry out a disciplined migration of an app on an older technology stack; designing and building the cloud environment is out of its scope. That boundary is why the two fit together: AIMF handles the app side, and my earlier FSx for ONTAP work covers the data and storage side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Differences from the previous article
&lt;/h2&gt;

&lt;p&gt;The previous article is "&lt;a href="https://dev.to/aws-builders/aws-transform-now-supports-block-storage-migration-to-fsx-for-ontap-benefits-and-pitfalls-from-a-1hhe"&gt;AWS Transform Now Supports Block Storage Migration to FSx for ONTAP — Benefits and Pitfalls from a Hands-On EC2 Test&lt;/a&gt;". This post differs in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope: the previous article covered getting data onto FSx for ONTAP (AWS Transform migrating block storage). This post leaves the data where it is and moves only the app's code&lt;/li&gt;
&lt;li&gt;What was measured: the previous article measured a migration job. This post measures how five behaviors differ when the same volume is opened over SMB, NFS, and S3 Access Points&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Staged design and the single-volume constraint
&lt;/h2&gt;

&lt;p&gt;I fixed the data on one volume so that failures in the app migration and failures in the data migration would not get mixed together. With the volume fixed, an observed difference can be read as a difference between paths. That choice to fix the volume is a design decision, not a measurement result. Fixing the volume does not mean there can be no working copies. FlexClone, a way to add a working volume while keeping appdata fixed, is described in the second half of this section.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Where the code comes from&lt;/th&gt;
&lt;th&gt;Volume&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Windows Amazon EC2 instance, .NET Framework 4.8 (a Linux Amazon EC2 instance opens SMB over cifs as the Probe's second measuring client)&lt;/td&gt;
&lt;td&gt;SMB&lt;/td&gt;
&lt;td&gt;The original app&lt;/td&gt;
&lt;td&gt;appdata (ntfs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The stage 0 setup; the Linux Amazon EC2 instance also opens NFS 4.1&lt;/td&gt;
&lt;td&gt;NFS 4.1 added&lt;/td&gt;
&lt;td&gt;The original app, plus a Python version of the Probe on the Linux side&lt;/td&gt;
&lt;td&gt;appdata (still ntfs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Linux Amazon EC2 instance, .NET 10&lt;/td&gt;
&lt;td&gt;NFS 4.1 and cifs&lt;/td&gt;
&lt;td&gt;AIMF (main line), atx (comparison)&lt;/td&gt;
&lt;td&gt;appdata (still ntfs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AWS Lambda (in the VPC), .NET 10&lt;/td&gt;
&lt;td&gt;S3 Access Points&lt;/td&gt;
&lt;td&gt;From AIMF; from atx (first run; run id s3)&lt;/td&gt;
&lt;td&gt;appdata (still ntfs)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/reproduce.md#L69-L110" rel="noopener noreferrer"&gt;the stage table in docs/en/reproduce.md&lt;/a&gt; (commit-pinned; I reorganized the columns)&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy54z9xyj5ay5tfmecqy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy54z9xyj5ay5tfmecqy.png" alt="Stage flow diagram. Windows over SMB, Linux over NFS, Linux over NFS and cifs, and Lambda over S3 Access Points all connect to one volume, appdata." width="800" height="981"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1: The flow from stage 0 to stage 3. The volume in the bottom row stays the same. The container migration is a branch and was not measured.&lt;/em&gt; (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/f5ba16aaccfd06023fd5dbc770c573c42ad493ce/docs/_assets/images/png/appmod-stage-flow-en-dark@2x.png" rel="noopener noreferrer"&gt;dark theme version&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The point of the figure: the volume in the bottom row stays one volume as the stages advance.&lt;/p&gt;

&lt;p&gt;AIMF is the main line of stage 2. AIMF's first step (Phase 0a) imported the atx analysis results. I ran the atx dotnet-modernization transformation separately, for comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handing out working volumes with FlexClone (not done in this verification)
&lt;/h3&gt;

&lt;p&gt;For app development and testing, you often need a separate place to read and write production-like data (in this post, the contents of appdata) without modifying the data itself. ONTAP FlexClone is one way to add that place without copying all of the data. FlexClone can be used alongside the design that fixes appdata as a single volume.&lt;/p&gt;

&lt;p&gt;A FlexClone is a feature that creates a writable point-in-time copy by referencing the metadata of a Snapshot (the state of a volume at one moment). I call the copy a clone, and I call the volume it is made from the source volume (the NetApp documentation calls it the parent volume; in this post the source volume is appdata). The clone shares data blocks with the source volume, and until you write to the clone it uses capacity only for metadata (&lt;a href="https://docs.netapp.com/us-en/ontap/concepts/flexclone-volumes-files-luns-concept.html" rel="noopener noreferrer"&gt;ONTAP: FlexClone volumes, files, and LUNs concepts&lt;/a&gt;). The free space a newly created clone uses is described as about 0.5% of the clone's nominal size. New data written to either the source volume or the clone is not shared between the two volumes, and the clone's usage grows by the amount written to the clone (&lt;a href="https://docs.netapp.com/us-en/ontap/volumes/determine-space-used-flexclone-task.html" rel="noopener noreferrer"&gt;ONTAP: space used by a FlexClone volume&lt;/a&gt;). The NetApp documentation contrasts this with traditional copies, which can take minutes to hours, and says that because FlexClone shares blocks, a copy of a large dataset can be made in a short time (this post gives no creation-time number).&lt;/p&gt;

&lt;h4&gt;
  
  
  How this project used it
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Designed and implemented: the verification script &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/scripts/ontap/integration-clone.sh" rel="noopener noreferrer"&gt;&lt;code&gt;integration-clone.sh&lt;/code&gt;&lt;/a&gt; creates and deletes, over the ONTAP REST API, a FlexClone named &lt;code&gt;appdata_it_&amp;lt;n&amp;gt;&lt;/code&gt; from a Snapshot of appdata. The script never creates, changes, or deletes appdata itself. Its purpose is to create and remove the clones that the AIMF Phase 3 integration test (the integration gate) uses&lt;/li&gt;
&lt;li&gt;Dry run only: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/scripts/tests/dryrun_shell_tests.sh" rel="noopener noreferrer"&gt;&lt;code&gt;dryrun_shell_tests.sh&lt;/code&gt;&lt;/a&gt; checks the requests that were assembled and the name validation, without calling ONTAP. No clone is created&lt;/li&gt;
&lt;li&gt;A check at teardown only: in steps 4 and 5 of the environment teardown, I confirmed that no FlexClone and no &lt;code&gt;it_*&lt;/code&gt; Snapshot remained and that the recovery queue was empty (&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/environment.md" rel="noopener noreferrer"&gt;docs/en/environment.md&lt;/a&gt;). This confirms absence; it is not evidence that one was created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I have confirmed the design and the command assembly, but I have not created a clone in a real environment or measured behavior on one.&lt;/p&gt;

&lt;h4&gt;
  
  
  Use cases the documentation describes
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;What the documentation says (paraphrased)&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Copies for development and testing&lt;/td&gt;
&lt;td&gt;Described as temporary, writable copies for trying an app against a production dataset&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.netapp.com/us-en/ontap/concepts/flexclone-volumes-files-luns-concept.html" rel="noopener noreferrer"&gt;ONTAP concepts&lt;/a&gt;, &lt;a href="https://docs.netapp.com/us-en/ontap/task_admin_clone_data.html" rel="noopener noreferrer"&gt;ONTAP: clones for testing&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several copies, per user or per server&lt;/td&gt;
&lt;td&gt;Several copies of the same dataset (the documented example is virtual desktop deployment). An example that makes several clones from the same Snapshot for several development and test servers. A use that gives additional users access to the data without giving them access to production data. I found no document that says "one per developer"&lt;/td&gt;
&lt;td&gt;The two in the first row, &lt;a href="https://aws.amazon.com/blogs/storage/replicate-and-clone-sap-hana-databases-with-amazon-fsx-for-netapp-ontap/" rel="noopener noreferrer"&gt;AWS: SAP HANA replicate and clone&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;documented (per-developer use not stated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rehearsal before a change&lt;/td&gt;
&lt;td&gt;An example of trying a database operation on a clone before running it against production. Checks before an application or Kubernetes cluster upgrade. Validation of a system upgrade or migration&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/blogs/storage/accelerate-development-refresh-cycles-and-optimize-cost-with-amazon-fsx-for-netapp-ontap-cloning/" rel="noopener noreferrer"&gt;AWS: refreshing a development environment&lt;/a&gt;, &lt;a href="https://aws.amazon.com/blogs/storage/run-containerized-applications-efficiently-using-amazon-fsx-for-netapp-ontap-and-amazon-eks/" rel="noopener noreferrer"&gt;AWS: applications on Amazon EKS&lt;/a&gt;, the SAP HANA post in the second row&lt;/td&gt;
&lt;td&gt;documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checking recovery and incident response&lt;/td&gt;
&lt;td&gt;A use for disaster recovery (DR) testing. Mounting a clone of a Snapshot taken before data corruption on another server to attempt a logical repair&lt;/td&gt;
&lt;td&gt;The SAP HANA post in the second row&lt;/td&gt;
&lt;td&gt;documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provisioning test data for CI&lt;/td&gt;
&lt;td&gt;The documents I could fetch contain no statement that names use in CI. This use is only my extension of "several copies" above&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;hypothesis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics and reporting on a point-in-time copy&lt;/td&gt;
&lt;td&gt;The documents I could fetch contain no statement that names analytics or reporting&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;hypothesis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The examples in the sources are Oracle Database (Amazon EC2, NFS), MySQL (Amazon EKS, iSCSI), and SAP HANA. The documents I fetched have no example of a Windows app opening an SMB NTFS share.&lt;/p&gt;

&lt;h4&gt;
  
  
  Relationship to the single fixed volume
&lt;/h4&gt;

&lt;p&gt;Through stages 0 to 3, appdata stays one volume with the same UUID. A FlexClone is a separate volume from appdata, and you can specify a separate junction path for it (the path that attaches a volume to the SVM namespace) (&lt;a href="https://docs.netapp.com/us-en/ontap-cli/volume-clone-create.html" rel="noopener noreferrer"&gt;ONTAP CLI: volume clone create&lt;/a&gt;). So a clone does not contradict the fixed design. It adds a working volume, per developer or per stage, next to the fixed appdata. On the other hand, every clone adds a new junction path. The five behaviors this post measured (case sensitivity, names that contain &lt;code&gt;\&lt;/code&gt;, exclusive locking, the writability check, and write visibility) and the treatment of root are properties of the access path. So you measure them again on each path that opens a clone, the same as the takeaway "measure again each time you add a path".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;One fixed volume (this verification, verified)&lt;/th&gt;
&lt;th&gt;FlexClone working volume (documented, not done)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What you get&lt;/td&gt;
&lt;td&gt;You can attribute differences to the path. The UUID, the security style, and the seed sha256 did not change across stages 0 to 3&lt;/td&gt;
&lt;td&gt;A write target per user or per purpose that does not modify appdata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior per path&lt;/td&gt;
&lt;td&gt;The five behaviors are measured (one environment)&lt;/td&gt;
&lt;td&gt;Needs to be measured again on each path that opens the clone. Opening a clone of an NTFS volume over SMB, NFS, and S3 Access Points is not measured. I found no statement in the fetched documents on how NTFS ACLs or SMB shares carry over to a clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity&lt;/td&gt;
&lt;td&gt;No additional capacity, because no volume besides appdata is added&lt;/td&gt;
&lt;td&gt;Metadata right after creation, then growth by the amount written. After a split (detaching the clone from its source volume to make an independent, normal volume), both the source volume and the clone need the full capacity set by the volume's guarantee (&lt;a href="https://docs.netapp.com/us-en/ontap/volumes/split-flexclone-from-parent-task.html" rel="noopener noreferrer"&gt;ONTAP: split&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency on the source volume&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;You cannot delete the source volume while the clone exists (&lt;a href="https://docs.netapp.com/us-en/ontap/volumes/create-flexclone-task.html" rel="noopener noreferrer"&gt;ONTAP: creating a FlexClone&lt;/a&gt;). A split cannot be undone, and no new Snapshot of that clone can be created during the split&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What remains after deletion&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;In a separate verification of mine (2026-08-26, ONTAP 9.18.1P3D1, ap-northeast-1, a 1 GiB volume with the UNIX security style, one observation), a clone deleted in ONTAP stayed in the recovery queue, the deletion of the source volume it was made from failed, and a purge cleared it (&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/91178f7f792ff9b4e26be60735e60d0f68aa511b/docs/ja/reference/limits/s3ap-interoperability.md" rel="noopener noreferrer"&gt;measurement of ONTAP features over S3 Access Points&lt;/a&gt;, Japanese, carried-over)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of volumes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The volume limit per FSx for ONTAP file system is 500 (first-generation, and second-generation with one HA pair) or 1,000 (second-generation with two or more HA pairs), and 491 and 975 respectively when you use S3 Access Points (975 is for two HA pairs and decreases as HA pairs are added) (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/limits.html" rel="noopener noreferrer"&gt;AWS: quotas&lt;/a&gt;). That page does not say whether a clone counts toward the number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How it appears on the AWS side&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;A change made on the ONTAP side can take up to several minutes to appear in the AWS console, CLI, and API (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/managing-resources-ontap-apps.html" rel="noopener noreferrer"&gt;AWS: managing with ONTAP applications&lt;/a&gt;). In the separate verification described in the "What remains after deletion" row, a clone took 1,177 seconds (observed every 20 seconds, one run) to appear in the AWS-side list, and in another run it had not appeared at 1,258 seconds (carried-over)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where you create it&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;The ONTAP CLI and REST API (&lt;a href="https://docs.netapp.com/us-en/ontap-restapi/post-storage-volumes.html" rel="noopener noreferrer"&gt;ONTAP REST: creating a volume&lt;/a&gt;). The AWS API's ONTAP volume-creation settings list no field for specifying a clone (&lt;a href="https://docs.aws.amazon.com/fsx/latest/APIReference/API_CreateOntapVolumeConfiguration.html" rel="noopener noreferrer"&gt;AWS API: CreateOntapVolumeConfiguration&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationship to backups&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;In an AWS Transform VMware migration example (a different feature from the atx in this post), locked Snapshots that FSx for ONTAP backups create can block a split (&lt;a href="https://aws.amazon.com/jp/blogs/storage/migrate-vmware-storage-to-amazon-fsx-for-netapp-ontap-using-aws-transform/" rel="noopener noreferrer"&gt;AWS: VMware migration&lt;/a&gt;). A clone that is in a data protection relationship cannot be split&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also clone a FlexClone (&lt;a href="https://docs.netapp.com/us-en/ontap/volumes/flexclone-efficient-copies-concept.html" rel="noopener noreferrer"&gt;ONTAP: efficient copies&lt;/a&gt;). In the table above, I did not confirm the behavior of a clone of an NTFS volume or the relationship to backups.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to choose
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Where it fits: you need working copies, per user or per purpose, that you can write to while keeping the contents of appdata, and the time and capacity of making full copies is a burden&lt;/li&gt;
&lt;li&gt;Where to consider other means: if you need only a few working copies for a short time, an ordinary copy is enough. If you keep a clone for a long time and writes grow, the clone's usage grows and the period during which the source volume cannot be deleted gets longer. Also consider other means when the volume count is near the limit, or when you cannot build the ONTAP CLI and REST API into your operations. Restoring from a backup (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/to-restore-backups.html" rel="noopener noreferrer"&gt;AWS: restoring backups&lt;/a&gt;) is also a way to create a separate volume. The documents I fetched do not say whether a restore shares blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you can decide: whether to take the approach of adding working volumes with FlexClone while keeping appdata fixed as one volume, and, if you do, whether you can accept the operational costs in the table above (deleting the source volume, splitting, the volume count, and the delay before changes appear on the AWS side).&lt;/p&gt;

&lt;h3&gt;
  
  
  The value of S3 Access Points and its four prerequisites
&lt;/h3&gt;

&lt;p&gt;The value of using S3 Access Points is that an S3 API client such as Lambda reaches the same volume without copying the data. In this environment, the 5 reports and &lt;code&gt;index.json&lt;/code&gt; written by the Lambda Worker were visible from the NFS side (stage 3, one observation). There are four prerequisites:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Prerequisite&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Evidence tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Fixed NetworkOrigin&lt;/td&gt;
&lt;td&gt;Specified only at creation. To change it, delete the access point and create it again&lt;/td&gt;
&lt;td&gt;documented (AWS documentation; the source is in the stage 3 chapter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;The user (identity) that handles S3 Access Points requests on the volume&lt;/td&gt;
&lt;td&gt;In this environment, the WINDOWS user &lt;code&gt;appsvc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;verified (stage 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Reachability to an AD domain controller (DC)&lt;/td&gt;
&lt;td&gt;In this environment I used the access point with the DC reachable. Whether every data operation on an AD-joined SVM requires DC reachability, I have not confirmed. A successful HeadBucket does not show that file operations work&lt;/td&gt;
&lt;td&gt;open (I could not find a record a reader can reproduce, or a public primary source; details are in "What has not been measured yet")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;No exclusive locking and no event notifications&lt;/td&gt;
&lt;td&gt;file-locking is skipped in the Probe run through S3 Access Points; the trigger that starts the Worker is polling from EventBridge Scheduler&lt;/td&gt;
&lt;td&gt;The file-locking skip is verified. The unavailability of event notifications is documented (my reading of the AWS API support table, which lists GetBucketNotificationConfiguration as not supported; see the stage 3 chapter)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What you can decide: whether to adopt the approach of changing only the paths (SMB, then NFS, then S3 Access Points) without moving the data, and whether you can confirm the four prerequisites above in your own environment before you use S3 Access Points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test environment and method
&lt;/h2&gt;

&lt;p&gt;I measured three paths (Windows, Linux, Lambda) against the same volume, appdata, in the same SVM. The setup is below (as in the caveats stated at the start).&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;Unless noted otherwise, the tier is verified (versions are values read from ONTAP REST and similar).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Region and configuration&lt;/td&gt;
&lt;td&gt;ap-northeast-1, SINGLE_AZ_1, 1 HA pair, 1,024 GiB SSD, 128 MBps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP&lt;/td&gt;
&lt;td&gt;9.19.1P3 (the first build was 9.19.1P2; I did not look into why it was updated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVM and volume&lt;/td&gt;
&lt;td&gt;1 SVM, 1 volume appdata, security style ntfs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active Directory&lt;/td&gt;
&lt;td&gt;AWS Managed Microsoft AD (Standard, 2 DCs). The directory needs 2 AZs, so I added a second subnet to the VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows client&lt;/td&gt;
&lt;td&gt;Amazon EC2 t3.large. I did not record the Windows Server version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux client&lt;/td&gt;
&lt;td&gt;Amazon EC2 t3.medium, Amazon Linux 2023 x86_64. NFS 4.1 &lt;code&gt;sec=sys&lt;/code&gt; (uid 10001), cifs &lt;code&gt;vers=3.0&lt;/code&gt;. I did not record the kernel or cifs-utils versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Around the VPC&lt;/td&gt;
&lt;td&gt;6 interface endpoints, an S3 gateway endpoint, 4 Secrets Manager secrets, an Amazon S3 artifacts bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool versions&lt;/td&gt;
&lt;td&gt;AIMF v0.11.0 (pinned tag), kiro-cli 2.29.1, atx 3.18.0, .NET SDK 10.0.400 (runtime 10.0.11; self-contained on Linux), AWSSDK.S3 4.0.104.2 (pinned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The app, DocIntake&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8, C#, 0 NuGet dependencies, 10 .cs files, 935 lines (sum of &lt;code&gt;wc -l&lt;/code&gt;; atx reported 15 files and 1,089 lines). Two console apps (a Worker that processes files and writes reports and &lt;code&gt;index.json&lt;/code&gt;, and the Probe) and one class library. &lt;code&gt;IFileStore&lt;/code&gt; has 5 methods&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvficutsu39g56vk0vim.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvficutsu39g56vk0vim.png" alt="Environment diagram. A VPC contains FSx for ONTAP, AWS Managed Microsoft AD, Windows and Linux EC2 instances, and Lambda, which reach the volume over SMB, NFS 4.1, and the S3 API." width="800" height="1157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2: The test environment. Arrow labels name the path or purpose (SMB, NFS 4.1, S3 API, fetch credentials, domain join).&lt;/em&gt; (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/f5ba16aaccfd06023fd5dbc770c573c42ad493ce/docs/_assets/images/png/environment-architecture-en-dark@2x.png" rel="noopener noreferrer"&gt;dark theme version&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The point of the figure: all three paths reach the same volume in the same SVM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Probe: a console app that measures five behaviors and records them as JSON&lt;/li&gt;
&lt;li&gt;Positive control for case-sensitivity: in addition to a lookup that refers to an existing file with different letter case, a lookup that refers to it exactly as it exists, to confirm the measurement works&lt;/li&gt;
&lt;li&gt;Standalone and pair: standalone runs the Probe on one machine; a pair links two machines by a shared sync ID (locking and visibility need two machines)&lt;/li&gt;
&lt;li&gt;Stage boundaries b0 to b3 and &lt;code&gt;check-invariant&lt;/code&gt;: records of the volume's state taken after each stage (b0 after stage 0, b1 to b3 after stages 1 to 3). Each record holds the volume UUID, security style, sha256 of the 6 seed files, export policy, snapshots, and snapshot locking. &lt;code&gt;check-invariant&lt;/code&gt; compares a later record with b0 and checks three items: the UUID, the seed sha256, and the top-level paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Evidence labels and run ids
&lt;/h3&gt;

&lt;p&gt;The labels in the tier column are as follows. verified: a value I measured in a named environment (with count and date). field-observation: a value I saw once and did not reproduce. documented: a value stated in a document whose source I read in full. carried-over: a value measured in another verification of mine that I did not measure this time. hypothesis: a guess. open: an item for which I could not find a record a reader can reproduce or a public primary source. not measured: an item I did not measure.&lt;/p&gt;

&lt;p&gt;The main runs are below (a run id identifies one measurement).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;run id&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What it measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;s0-20261011T065443Z&lt;/td&gt;
&lt;td&gt;2026-10-11&lt;/td&gt;
&lt;td&gt;All 5 behaviors in stage 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s1-20261011T075058Z&lt;/td&gt;
&lt;td&gt;2026-10-11&lt;/td&gt;
&lt;td&gt;All 5 behaviors in stage 1 (after adding NFS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s4c-20261012T034403Z&lt;/td&gt;
&lt;td&gt;2026-10-12&lt;/td&gt;
&lt;td&gt;case-sensitivity (SMB, cifs, NFS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s3b-20261012T022900Z&lt;/td&gt;
&lt;td&gt;2026-10-12&lt;/td&gt;
&lt;td&gt;case-sensitivity (S3 Access Points)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s0w-20261012T102314Z&lt;/td&gt;
&lt;td&gt;2026-10-12&lt;/td&gt;
&lt;td&gt;Output of the old Windows Worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2m-20261012T090934Z&lt;/td&gt;
&lt;td&gt;2026-10-12&lt;/td&gt;
&lt;td&gt;8 pairs (4 lock, 4 visibility) of the Probe converted to .NET 10 in stage 2 (Linux side; the Windows side was the earlier build)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s2n-20261012T104314Z&lt;/td&gt;
&lt;td&gt;2026-10-12&lt;/td&gt;
&lt;td&gt;A run that re-took the shape of the writability check (acl-evaluation) record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s3m-20261012T104800Z&lt;/td&gt;
&lt;td&gt;2026-10-12&lt;/td&gt;
&lt;td&gt;The Probe and Worker converted to .NET 10, on Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The source of this table is the run id naming in docs/en/reproduce.md, the same document as the stage table (I reorganized the columns).&lt;/p&gt;

&lt;p&gt;The runs whose case-sensitivity values I invalidated are s0, s1, s2, s2f, s2g, and the first stage 3 run, s3. They had no case-sensitivity positive control, so I use none of those case-sensitivity values in any conclusion. The values of the other behaviors in those runs are valid. Source: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/reproduce.md#L391-L396" rel="noopener noreferrer"&gt;record validity in docs/en/reproduce.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The measurement window is 2026-10-11 to 2026-10-12. Times in the tables mix UTC and JST, and each row says which. For external unit prices and availability, I give the date I read them.&lt;/p&gt;

&lt;p&gt;What you can decide: if your environment differs from the configuration table (versions, Multi-AZ, security style), that is a range this post did not measure. Look up the matching item in "When this post fits and when it does not" and "What this post covers and what it does not", and treat it as an item to measure again in your own environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stages 0 and 1: unchanged UUID and seed, changed root handling
&lt;/h2&gt;

&lt;p&gt;Adding NFS to the SMB volume did not change the volume UUID, the security style, or the sha256 of the seed. What changed was how root is treated over NFS and how to read the mode bits in &lt;code&gt;ls -l&lt;/code&gt; (2026-10-11, one observation each).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unchanged&lt;/th&gt;
&lt;th&gt;Changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The seed sha256, the volume UUID, and the security style (still ntfs before and after)&lt;/td&gt;
&lt;td&gt;As root over NFS, &lt;code&gt;cd /mnt/appdata&lt;/code&gt; gave Permission denied, while uid 10001 could read and write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;check-invariant&lt;/code&gt; passed for b0 against b1 (UUID, seed, and top-level paths matched)&lt;/td&gt;
&lt;td&gt;Over NFS, &lt;code&gt;ls -la&lt;/code&gt; showed the top level as &lt;code&gt;d---------&lt;/code&gt; (owned by root), yet uid 10001 could read and write. Files created over SMB showed as &lt;code&gt;-rwx------&lt;/code&gt;, owner &lt;code&gt;nobody&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Stage 0 (SMB only): I created the AD users &lt;code&gt;appsvc&lt;/code&gt; and &lt;code&gt;appreader&lt;/code&gt; and placed the 6 seed files over SMB. The sha256 values read back matched the originals, and the Probe finished with the 2 standalone runs (Windows and the Linux cifs mount) and all 4 pairs (2 lock, 2 visibility) at Success&lt;/li&gt;
&lt;li&gt;Stage 1: I changed the export policy to the NFS 4 family, &lt;code&gt;sec=sys&lt;/code&gt;, and &lt;code&gt;superuser none&lt;/code&gt; (a setting that replaces NFS root with an anonymous user). I created the UNIX users &lt;code&gt;appsvc&lt;/code&gt; (10001) and &lt;code&gt;appreader&lt;/code&gt; (10002) and put 4 name mappings in place (2 win_unix, 2 unix_win). The Probe finished with the 3 standalone runs (Windows, the Linux cifs mount, and Linux NFS) and all 8 pairs (4 lock, 4 visibility) at Success&lt;/li&gt;
&lt;li&gt;Reading &lt;code&gt;ls -l&lt;/code&gt;: on ntfs, the mode bits are a mapping of the ACL and do not tell you whether a write is allowed (this reading is a hypothesis). ADR-3 decides the Linux writability check by whether a real I/O attempt succeeds, not by the mode bits (a design decision; see the stage 2 chapter)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note&lt;/strong&gt;&lt;br&gt;
I read the denial of NFS root as an effect of the export policy's &lt;code&gt;superuser none&lt;/code&gt;, but that is an inference from the setting (hypothesis). I did not try NFS without name mappings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This chapter gives no case-sensitivity numbers. The stage 0 and 1 records have no positive control, so they support no conclusion (the re-measurement is in "Behavior by access path").&lt;/p&gt;

&lt;p&gt;Sources for this chapter: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-0-smb-ntfs.md#L70-L116" rel="noopener noreferrer"&gt;the stage 0 re-measurement&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-1-multiprotocol.md#L111-L142" rel="noopener noreferrer"&gt;the two-client behaviors in stage 1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you can decide: when you add NFS to an existing SMB setup, this environment needed no data migration. Put the procedures that use root, and the procedure of checking permissions with &lt;code&gt;ls -l&lt;/code&gt;, on your review list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: atx up to net10.0, AIMF up to ADRs and a ledger
&lt;/h2&gt;

&lt;p&gt;atx got as far as a net10.0 build, but five legacy-code habits (ways of writing that assume Windows and SMB) remained after conversion. AIMF produced ADR-1 to 5 and ledger AP-1 to 14 and went on to implement NFS and S3 Access Points. The scopes differ.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Run date and duration&lt;/th&gt;
&lt;th&gt;Unit and cost&lt;/th&gt;
&lt;th&gt;Output and result check&lt;/th&gt;
&lt;th&gt;Who made the fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;atx analysis (comprehensive-codebase-analysis)&lt;/td&gt;
&lt;td&gt;2026-10-11 09:48 to 10:03 UTC (about 15 min), exit 0&lt;/td&gt;
&lt;td&gt;92.46 / 120.00 agent minutes (atx's billing unit), about $3.24 (estimate)&lt;/td&gt;
&lt;td&gt;28 &lt;code&gt;ATXDocumentation&lt;/code&gt; files, no code changes. The gitleaks scan before upload found no leaks&lt;/td&gt;
&lt;td&gt;Analysis only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;atx dotnet-modernization&lt;/td&gt;
&lt;td&gt;2026-10-11 11:05 to 11:25 UTC (about 20 min), exit 0&lt;/td&gt;
&lt;td&gt;139.00 / 300 agent minutes (atx's record). In the hourly Cost Explorer data, 11.025 minutes were billable (about $0.39, Estimated); the other 127.97 minutes do not appear as billable (breakdown below the table in "Costs that could not be attributed, and why")&lt;/td&gt;
&lt;td&gt;11 files, +55/−162. net10.0, SDK-style csproj, settings moved to &lt;code&gt;IConfiguration&lt;/code&gt; and &lt;code&gt;appsettings.json&lt;/code&gt;. The parity gate (a check that the old and converted code agree on Config, Boot, Endpoints, and Auth) passed on all 4 items. Build succeeded on macOS with 0 errors and 7 warnings&lt;/td&gt;
&lt;td&gt;atx did the conversion. The Linux fixes were my separate work (not an atx feature)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AIMF (Phase 0a to 4)&lt;/td&gt;
&lt;td&gt;2026-10-12 13:50 to 17:23 JST (3 h 33 min; see ※)&lt;/td&gt;
&lt;td&gt;284.26 Kiro credits (AIMF's billing unit; limit 5,000, notification at 4,000). Not converted to dollars&lt;/td&gt;
&lt;td&gt;ADR-1 to 5, ledger AP-1 to 14. Build warnings 0, 8 tests passed (later 54), a self-contained Linux publish output of 196 files (81 MB); the sha256 of all 196 files matched after deployment&lt;/td&gt;
&lt;td&gt;AIMF did the Path.Combine change and the real-I/O writability check. AIMF ported my Probe fix that adds the positive control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;※ The 3 h 33 min is the span from the first to the last commit in the records repository. It is not the session length. The log starts with an approval at 13:25:54, and the span includes waiting for my decisions.&lt;/p&gt;

&lt;p&gt;Sources for this table: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-2-aimf-modern-dotnet.md#L190-L197" rel="noopener noreferrer"&gt;the atx and AIMF comparison in stage 2&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-2-aimf-modern-dotnet.md#L310-L335" rel="noopener noreferrer"&gt;cost estimates and observed costs in stage 2&lt;/a&gt; (I reorganized the columns for this post)&lt;/p&gt;

&lt;p&gt;The five legacy-code habits that remained after the atx conversion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The backslash join in &lt;code&gt;StorePaths.Combine&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Docs\Index.json&lt;/code&gt; reference&lt;/li&gt;
&lt;li&gt;ACL: &lt;code&gt;File.GetAccessControl&lt;/code&gt; was only replaced by &lt;code&gt;FileInfo.GetAccessControl&lt;/code&gt;, which does not work on Linux. No NfsFileStore was added either&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;FileShare.None&lt;/code&gt; exclusive lock&lt;/li&gt;
&lt;li&gt;The assumption that a file can be read back right after it is written&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I ran the code that atx converted on Linux (2026-10-11, self-contained, one observation), over NFS, the path-separator, file-locking, and write-visibility behaviors failed with &lt;code&gt;UnauthorizedAccessException&lt;/code&gt;, and acl-evaluation failed with &lt;code&gt;PlatformNotSupportedException&lt;/code&gt;. On the Linux cifs mount side, &lt;code&gt;Combine&lt;/code&gt; created names joined with &lt;code&gt;\&lt;/code&gt; directly under &lt;code&gt;/mnt&lt;/code&gt; (a local xfs), outside the mount, and nothing was written to the share.&lt;/p&gt;

&lt;p&gt;The AIMF ledger, verification, and decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ledger: of AP-1 to AP-14, 12 resolved, 1 replaced by an alternative implementation, 1 not needed, 0 open&lt;/li&gt;
&lt;li&gt;Negative tests of the detectors for AP-1 to AP-13 (I injected a temporary violating file and checked that it was detected): 13 pass, 0 fail&lt;/li&gt;
&lt;li&gt;Independent verification: one run by a separate agent with a separate context. It found no missing functionality and raised one scope point: &lt;code&gt;OpenExclusive&lt;/code&gt; cannot be used on S3, and no one had approved that limitation. AP-14 and ADR-5 closed the point&lt;/li&gt;
&lt;li&gt;ADRs: ADR-2 puts performance out of scope. ADR-3 makes the Linux writability check a real I/O attempt instead of a pre-check. ADR-4 makes &lt;code&gt;index.json&lt;/code&gt; compact System.Text.Json output (ADR-4 was my decision)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FileShare.None&lt;/code&gt;: AIMF also kept this exclusive lock on purpose (AP-4)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conditions of the comparison: the scopes differ (atx goes up to the .NET 10 conversion and the parity gate; AIMF goes up to implementing NFS and S3 Access Points), and running AIMF depends on the atx analysis. I measured one app, once per tool, and the cost units are not converted, as the table shows.&lt;/p&gt;

&lt;p&gt;What you can decide: if you only need to raise the .NET version and check agreement with the old code, atx's scope is enough. If you also need a per-path implementation and a record of decisions, that is AIMF's scope (how to choose is in the later chapter "Choosing between atx and AIMF").&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: Lambda reaching the same volume through S3 Access Points
&lt;/h2&gt;

&lt;p&gt;A Lambda Worker (.NET 10, in the VPC) wrote 5 reports and &lt;code&gt;index.json&lt;/code&gt; through S3 Access Points, and they were visible from the NFS side (one observation). Three conditions held when the Worker reached the volume: NetworkOrigin set to the VPC, the WINDOWS user &lt;code&gt;appsvc&lt;/code&gt; as the identity, and an AD domain controller (DC) that was reachable. I have not confirmed whether the DC must be reachable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attachment: &lt;code&gt;AWS::FSx::S3AccessPointAttachment&lt;/code&gt; reached CREATE_COMPLETE, and Lifecycle was AVAILABLE&lt;/li&gt;
&lt;li&gt;NetworkOrigin: VPC. Changing it means deleting and recreating the access point (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/access-points-for-fsxn-vpc.html" rel="noopener noreferrer"&gt;Creating access points restricted to a virtual private cloud&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/configuring-network-access-for-s3-access-points.html" rel="noopener noreferrer"&gt;Configuring network access for Amazon S3 access points&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Stack creation, invocation, and deletion: 3 times (s3, s3b, s3m). The wait for &lt;code&gt;delete-stack&lt;/code&gt; took more than 20 minutes (one recorded case), and no attachment remained after deletion&lt;/li&gt;
&lt;li&gt;Lambda settings: 512 MB, 120-second timeout. The EventBridge Scheduler rule was created as DISABLED&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results of the runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;atx-derived code (first run): the Worker exited 0 and wrote 5 reports and &lt;code&gt;index.json&lt;/code&gt; to &lt;code&gt;out/s3/&lt;/code&gt;. The run took 2,505 ms (Init 361 ms) with peak memory of 115 MB. The Probe took 821 ms with peak memory of 120 MB&lt;/li&gt;
&lt;li&gt;AIMF-derived code (s3m): &lt;code&gt;index.json&lt;/code&gt; was 181 bytes (confirmed with &lt;code&gt;ls&lt;/code&gt;; I did not read the contents). The Probe gave 4 measured and 1 skipped (file-locking). Lambda ran only as the writer, so there is no write-visibility value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A name mapping that appeared only while the access point was attached (the per-path behavior is in the table of the next chapter, "Behavior by access path"):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name mapping &lt;code&gt;s3_win&lt;/code&gt; (replacement set to the AD domain's &lt;code&gt;appsvc&lt;/code&gt;): it appeared in ONTAP only while the access point was attached; ONTAP held 5 name mappings at b3 while attached and 4 after deletion and at b2. I have not confirmed in documentation whether the attach process creates &lt;code&gt;s3_win&lt;/code&gt; (one observation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not check this time whether the AD domain controller must be reachable (the first item in "What has not been measured yet"). Because S3 Access Points cannot use event notifications, I used polling from EventBridge Scheduler as the trigger. I read the table as meaning that event notifications are unavailable, because the AWS API support table lists GetBucketNotificationConfiguration as not supported (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/access-points-for-fsxn-object-api-support.html" rel="noopener noreferrer"&gt;Access point compatibility (FSx for ONTAP User Guide)&lt;/a&gt;, checked on 2026-10-21, documented). That table lists only some of the APIs, and it has no row that names event notifications as unsupported.&lt;/p&gt;

&lt;p&gt;Request charges in Tokyo are $0.00108 per 1,000 PUT, COPY, POST, and LIST requests, and $0.000029 per 1,000 GET and other requests. I read them on 2026-10-12 in the "S3 Access Points for FSx" tab of the English Amazon FSx for NetApp ONTAP pricing page, and on 2026-10-21 I opened the same tab in a browser and saw the same values. On the same day I also read them again in the Price List API under the &lt;code&gt;AmazonS3&lt;/code&gt; usage types &lt;code&gt;APN1-Requests-FSXONTAP-Tier1&lt;/code&gt; and &lt;code&gt;Tier2&lt;/code&gt;; the values were the same. 1,000,000 PUT or LIST requests come to $1.08, which is unit price × count arithmetic (the billed amount is unconfirmed).&lt;/p&gt;

&lt;p&gt;What stayed unchanged at the stage boundaries: the UUID, security style, seed, snapshots, and snapshot locking did not change from b0 to b3. The export policy attached to appdata changed once, from default at b0 to &lt;code&gt;appmod_nfs&lt;/code&gt; (set in stage 1) at b1, and did not change from b1 to b3. &lt;code&gt;check-invariant&lt;/code&gt; raised one finding at b2 (s2m) and at b3 (s3m), and passed at b3 after the deletion (the details are in failure 5).&lt;/p&gt;

&lt;p&gt;Sources for this chapter: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-3-s3-access-points.md#L103-L109" rel="noopener noreferrer"&gt;the table of the five behaviors in stage 3&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-3-s3-access-points.md#L122-L163" rel="noopener noreferrer"&gt;the effect of a &lt;code&gt;\&lt;/code&gt; key on the SMB side, and the name mapping that appears only while attached&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you can decide: whether Lambda can use the same volume without copying the data depends on whether you can meet the four prerequisites in "Staged design and the single-volume constraint" in your own environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Behavior by access path
&lt;/h2&gt;

&lt;p&gt;Even on one volume, case sensitivity, the handling of names containing &lt;code&gt;\&lt;/code&gt;, whether an exclusive lock was possible, and the writability check differed by path, and write visibility differed by direction. What stayed the same across the paths was the volume they reach, the seed sha256, and the Worker output as JSON values.&lt;/p&gt;

&lt;p&gt;A caution on reading: the implementation changed together with the path. The Windows column is the old build (.NET Framework 4.8) and the others are the modernized build (.NET 10), so some differences cannot be attributed to the path alone. Unless noted otherwise, the tier of each cell is verified.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Windows (earlier .NET Framework 4.8 build, SMB)&lt;/th&gt;
&lt;th&gt;Linux cifs (.NET 10)&lt;/th&gt;
&lt;th&gt;Linux NFS 4.1 (.NET 10, uid 10001)&lt;/th&gt;
&lt;th&gt;Lambda (.NET 10, S3 Access Points)&lt;/th&gt;
&lt;th&gt;Source runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Case sensitivity&lt;/td&gt;
&lt;td&gt;Case-insensitive (a reference with different letter case resolved: true)&lt;/td&gt;
&lt;td&gt;Case-insensitive (resolved: true)&lt;/td&gt;
&lt;td&gt;Case-sensitive (did not resolve: false)&lt;/td&gt;
&lt;td&gt;Case-sensitive (did not resolve: false)&lt;/td&gt;
&lt;td&gt;s4c, s3b. s2m, s2n, and s3m show the same values as s4c and s3b&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names containing &lt;code&gt;\&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Treats &lt;code&gt;\&lt;/code&gt; as a separator (1 run)&lt;/td&gt;
&lt;td&gt;May reject a name with &lt;code&gt;\&lt;/code&gt; (errno 22). Not re-observed with the post-AIMF Probe&lt;/td&gt;
&lt;td&gt;An ordinary character (Python version). The .NET Probe got "not found" for the same name (1 run each)&lt;/td&gt;
&lt;td&gt;The key &lt;code&gt;probe/sep\check-s3.txt&lt;/code&gt; could be written and read back. The first design, a key made only of &lt;code&gt;\&lt;/code&gt;, was created directly under the volume root and appeared in the SMB listing as &lt;code&gt;PROBES~1.TXT&lt;/code&gt; (field-observation, 1 run)&lt;/td&gt;
&lt;td&gt;s1, s2f, s2g, s2m, s3m, s3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exclusive locking&lt;/td&gt;
&lt;td&gt;With the holder (the side that opens first) and the contender (the side that opens afterwards) swapped between Windows and Linux, the contender was refused in both directions and succeeded after release (1 run per pair)&lt;/td&gt;
&lt;td&gt;Same result as the Windows column, in pairs with Windows&lt;/td&gt;
&lt;td&gt;Same result as the Windows column, in pairs with Windows&lt;/td&gt;
&lt;td&gt;skipped (no operation to open exclusively). not measured&lt;/td&gt;
&lt;td&gt;s2m (Python version: s0, s1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writability check&lt;/td&gt;
&lt;td&gt;Pre-evaluates the NTFS ACL: writable&lt;/td&gt;
&lt;td&gt;Pre-check skipped; real I/O, run as root, showed it writable&lt;/td&gt;
&lt;td&gt;Pre-check skipped; real I/O showed it writable&lt;/td&gt;
&lt;td&gt;Pre-check skipped; the PUT was allowed&lt;/td&gt;
&lt;td&gt;s2m, s2n, s3m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write visibility&lt;/td&gt;
&lt;td&gt;Takes part as one half of the pair with Linux (the values are in the two Linux columns, by direction)&lt;/td&gt;
&lt;td&gt;66 ms when Windows writes and cifs reads; 7,968 ms when cifs writes and Windows reads&lt;/td&gt;
&lt;td&gt;35 ms when Windows writes and NFS reads; 8,342 ms when NFS writes and Windows reads&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;s2m (the same shape, fast when Windows writes and slow in the other direction, in s1 and s0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: the source material is spread across the per-stage documents. I combined it into one table for this post, and the public repository has no such table (commit-pinned).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case sensitivity (SMB, cifs, NFS): &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-1-multiprotocol.md#L91-L95" rel="noopener noreferrer"&gt;the stage 1 table&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Exclusive locking and write visibility: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/stage-2-aimf-modern-dotnet.md#L233-L262" rel="noopener noreferrer"&gt;the stage 2 measurements of the modernized app&lt;/a&gt; (7,968 / 8,342 ms; Worker output 201 / 181 bytes)&lt;/li&gt;
&lt;li&gt;The S3 Access Points column, names containing &lt;code&gt;\&lt;/code&gt;, and name mapping: the links at the end of the stage 3 chapter (two tables in stage 3)&lt;/li&gt;
&lt;li&gt;The original tables for exclusive locking and visibility with two clients: the links at the end of the stages 0 and 1 chapter (stage 1)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two points are not in the table: identity (which user a request is treated as) and Worker output. For identity, the treatment of root is in the stages 0 and 1 chapter and the stage 3 chapter. For Worker output (&lt;code&gt;index.json&lt;/code&gt;), the old Worker wrote 201 bytes and the modernized Worker (Linux) that AIMF built wrote 181 bytes. The two &lt;code&gt;index.json&lt;/code&gt; files are equal as JSON values (JSON-equal), and the 20-byte difference is whitespace (one run each). The Lambda Worker exited 0 and wrote 181 bytes (confirmed with &lt;code&gt;ls&lt;/code&gt;; I did not read the contents). The source runs are s0w, s2m, and s3m.&lt;/p&gt;

&lt;p&gt;In the write-visibility measurement, the reading client tries every 100 ms (polling interval 100 ms) to read the content the writer wrote, so differences below 100 ms (35 and 66) cannot be told apart. The asymmetry, fast when Windows writes and slow when Linux writes, had the same shape in three runs (s0, s1, s2m). I did not isolate why only the direction where Linux writes and Windows reads takes about 8 seconds. The Linux writer was Python (with fsync) in s0 and s1 and .NET (without fsync) in s2m, so the implementation differed too. In a separate verification, the reader's NFS mount options alone changed the time until a deletion made with S3 DeleteObject became visible over NFS, from 7 ms (&lt;code&gt;actimeo=0&lt;/code&gt;) to 2,171 ms (default mount) (2026-08-09, ap-northeast-1, NFSv3, UNIX security style, concurrency 1, ONTAP version not identified, carried-over; &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/s3ap-nfs-visibility.md" rel="noopener noreferrer"&gt;measurement of S3 and NFS visibility on the same volume&lt;/a&gt;, Japanese). I have not confirmed whether that is the same cause as the 8 seconds in this post. Details of the exclusive-locking pairs, such as the result of the read-only open, are in the original stage 1 table.&lt;/p&gt;

&lt;p&gt;What you can decide: when you add a path, you can make these five behaviors an item to measure again for each added path. On case sensitivity, Windows and cifs were insensitive while NFS and S3 Access Points were sensitive, so do not apply the SMB result to NFS or S3 Access Points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight failures and the checks to put in place before measuring
&lt;/h2&gt;

&lt;p&gt;There were 8 failures. The largest was the case-sensitivity measurement with no positive control, which made the case-sensitivity result unusable as a conclusion in 6 runs. The other 7 concern record shapes, environment creation, and side effects of scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The eight failures
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Case sensitivity with no positive control: invalid in 6 runs (s0, s1, s2, s2f, s2g, and the first stage 3 run, s3)

&lt;ul&gt;
&lt;li&gt;Fact: I looked for the reference &lt;code&gt;Docs\Index.json&lt;/code&gt; at the volume root, but the actual file was &lt;code&gt;seed/docs/index.json&lt;/code&gt;. Even on case-insensitive Windows SMB the lookup resolved to false, and taken at face value this would have meant "SMB is case-sensitive"&lt;/li&gt;
&lt;li&gt;Fix: added the positive control and measured again as s4c and s3b&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;measured&lt;/code&gt; on the cifs mount side (SMB used from Linux; &lt;code&gt;measured&lt;/code&gt; is the state in which the Probe recorded that it could measure the behavior) that was not written to the share: s2 left 3 files directly under the local &lt;code&gt;/mnt&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Fact: the cifs-mount-side &lt;code&gt;measured&lt;/code&gt; did not mean a successful write to the share. It meant that 3 files had been created directly under the local &lt;code&gt;/mnt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fix: deleted the 3 files that had been created&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;topology: cross-host&lt;/code&gt;, which does not show two machines coordinating: the Probe converted to .NET 10 emitted it even when run standalone

&lt;ul&gt;
&lt;li&gt;Fact: the record's &lt;code&gt;topology&lt;/code&gt; held the fixed value &lt;code&gt;cross-host&lt;/code&gt;, which is supposed to show that two machines coordinated. The value was not derived from a sync check, so it is not evidence&lt;/li&gt;
&lt;li&gt;Fix: changed to a step that judges a pair only when the two records carry the same sync ID (&lt;code&gt;probe_merge.py&lt;/code&gt;). In s2m, all 8 pairs had empty mismatch reasons and were judged as pairs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A writability record that disagreed with ADR-3 (s2m), and the missing comparison baseline &lt;code&gt;out/s0/&lt;/code&gt; for the Worker output

&lt;ul&gt;
&lt;li&gt;Fact: the s2m record held only the pre-evaluation value, which did not match the shape in ADR-3 (pre-check skipped, decided by real I/O). The baseline &lt;code&gt;out/s0/&lt;/code&gt; did not exist either, so the s2m JSON-equality check was a comparison against reference values&lt;/li&gt;
&lt;li&gt;Fix: fixed the Probe and measured again as s2n to get the ADR-3 shape. I ran the old Windows Worker once and made the baseline as s0w, and the reference values (the &lt;code&gt;index.json&lt;/code&gt; written by the Worker that atx transformed) and the old Worker's &lt;code&gt;index.json&lt;/code&gt; were identical down to the bytes (201 bytes)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-invariant&lt;/code&gt; failing because of &lt;code&gt;PROBES~1.TXT&lt;/code&gt;: two boundaries, b2 and b3

&lt;ul&gt;
&lt;li&gt;Fact: the first S3 Probe design wrote a key made only of &lt;code&gt;\&lt;/code&gt; with no slash. My reading that the key made only of &lt;code&gt;\&lt;/code&gt; was mapped to the 8.3 short name &lt;code&gt;PROBES~1.TXT&lt;/code&gt; is an inference (&lt;code&gt;PROBES~1.TXT&lt;/code&gt; appeared before the NFS deletion and was gone after it, which supports the inference)&lt;/li&gt;
&lt;li&gt;Fix: after I moved the key under &lt;code&gt;probe/&lt;/code&gt;, the new key did not appear at the root. I deleted the file that had been created at the root with one NFS &lt;code&gt;rm&lt;/code&gt;, with my approval. The first attempt at the deletion script was aborted before deleting anything, because the root user could not see the volume's top-level directory (nothing changed)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AIMF's Phase 3 Step 1 (the step that creates the implementation tree) failing because the base branch (base-ref) &lt;code&gt;master&lt;/code&gt; could not be resolved: once

&lt;ul&gt;
&lt;li&gt;Fact: the base-ref &lt;code&gt;master&lt;/code&gt; could not be resolved in the implementation tree (a working clone), and the step failed&lt;/li&gt;
&lt;li&gt;Fix: reran with &lt;code&gt;origin/master&lt;/code&gt; and it succeeded&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Two kinds of failure while creating the base environment: the template size limit and a missing secret

&lt;ul&gt;
&lt;li&gt;Fact: &lt;code&gt;base.yaml&lt;/code&gt; was 52,466 bytes and exceeded the 51,200-byte limit for &lt;code&gt;--template-body&lt;/code&gt; (no resources were created). On another attempt, the Secrets Manager secrets did not exist, so the Directory resource reached CREATE_FAILED and the stack rolled back (no resources remained)&lt;/li&gt;
&lt;li&gt;Fix: &lt;code&gt;deploy.sh&lt;/code&gt; now puts the template in the deployment Amazon S3 bucket and passes it from there. I now create the secrets with &lt;code&gt;create-secrets.sh&lt;/code&gt; first and then run the base creation again&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A script that ignored &lt;code&gt;--help&lt;/code&gt; and ran: it created 4 Secrets Manager secrets (01:24 JST on 2026-10-13)

&lt;ul&gt;
&lt;li&gt;Fact: I called &lt;code&gt;create-secrets.sh&lt;/code&gt;, a script that does not parse arguments, with &lt;code&gt;--help&lt;/code&gt; to check its usage. It ignored &lt;code&gt;--help&lt;/code&gt;, ran its main process, and created 4 Secrets Manager secrets. About 7 hours after they were created, I deleted those 4 secrets. The cost of those 4 secrets is an estimate: $0.40 per secret per month, prorated by hours. 4 secrets × $0.40 × 7 hours ÷ 730 hours (taking a month as 730 hours) comes to about $0.015 (1.5 cents)&lt;/li&gt;
&lt;li&gt;Fix: added to the build a gate, &lt;code&gt;check_help_is_safe&lt;/code&gt;, that calls every script with &lt;code&gt;--help&lt;/code&gt; and fails if any script does anything&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Five preventive checks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Put the positive control in place before measuring (failure 1). Without a file to find, even SMB returned false&lt;/li&gt;
&lt;li&gt;Put the NFS mount in a script (the first Probe in stage 1 failed because NFS was not mounted; that failure is not counted among the 8 above)&lt;/li&gt;
&lt;li&gt;Check that the secrets exist before building the base environment (failure 7)&lt;/li&gt;
&lt;li&gt;Put S3 keys under &lt;code&gt;probe/&lt;/code&gt; (failure 5)&lt;/li&gt;
&lt;li&gt;Make "&lt;code&gt;--help&lt;/code&gt; does nothing" a build gate: the build checks that &lt;code&gt;--help&lt;/code&gt; executes nothing (failure 8)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source for this chapter: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/verification-method-lessons.md" rel="noopener noreferrer"&gt;why a measurement without a positive control is invalid&lt;/a&gt; (record validity is in the link in "Test environment and method")&lt;/p&gt;

&lt;p&gt;What you can decide: before your own measurements, you can put these five points on a checklist. The positive control is needed in every measurement whose result can end as "not found".&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing between atx and AIMF
&lt;/h2&gt;

&lt;p&gt;atx and AIMF cover different scopes, so the table does not say which is better. It lets you choose by the scope you need (the result of processing the same app once with each).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;atx&lt;/th&gt;
&lt;th&gt;AIMF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;A tool for code analysis and transformation&lt;/td&gt;
&lt;td&gt;A workflow that runs Phase 0a to 4 through kiro-cli&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input and output in this verification&lt;/td&gt;
&lt;td&gt;Input: the source. Output: 28 analysis documents and an 11-file transformation. The &lt;code&gt;index.json&lt;/code&gt; (201 bytes) written by the Worker that atx transformed matched, byte for byte, the &lt;code&gt;index.json&lt;/code&gt; written by the old Worker (.NET Framework 4.8) before the transformation&lt;/td&gt;
&lt;td&gt;The atx analysis in; ADRs, a ledger, an implementation, and tests out. The &lt;code&gt;index.json&lt;/code&gt; (181 bytes) written by the modernized Worker (Linux) that AIMF built is JSON-equal to the old Worker's &lt;code&gt;index.json&lt;/code&gt;; ADR-4 made the output compact, so the bytes are 20 whitespace bytes smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;About 15 min for analysis, about 20 min for the transformation (run time)&lt;/td&gt;
&lt;td&gt;3 h 33 min from the first to the last commit (not the session length; includes waits for my decisions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost unit&lt;/td&gt;
&lt;td&gt;agent minutes&lt;/td&gt;
&lt;td&gt;Kiro credits (not converted to dollars)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human involvement&lt;/td&gt;
&lt;td&gt;I ran it with &lt;code&gt;run-atx.sh&lt;/code&gt; (I had put in a guard that forbids running it directly from an agent)&lt;/td&gt;
&lt;td&gt;6 decisions and 13 manual approvals from me; the AIMF run was held 35 times, waiting for my decision, because the approver script did not approve automatically (reasons: a command not on the allowlist of operations it approves automatically, a match with a forbidden pattern, a cd outside the working directory, and so on; the count is the STOP lines in the execution log)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade-offs&lt;/td&gt;
&lt;td&gt;The output contains no NFS or S3 implementation and no decision records such as ADRs or a ledger. Five legacy-code habits remained&lt;/td&gt;
&lt;td&gt;It needs setup (a records repository, hooks, running only with &lt;code&gt;--agent migration&lt;/code&gt;, and &lt;code&gt;install.sh&lt;/code&gt; writing &lt;code&gt;.kiro/&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;). Credits cannot be converted to dollars, and the shape of a Probe record did not match its ADR (failure 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Duration and cost are measured differently and in different units, so I do not compare atx and AIMF as shorter or longer, or cheaper or more expensive. Not measured for either tool: run-to-run variation, larger apps, other languages, and maintainability.&lt;/p&gt;

&lt;p&gt;Source of this table: the same comparison table as in the stage 2 chapter (I reorganized the columns).&lt;/p&gt;

&lt;p&gt;How to choose, in three points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If moving to .NET 10 and checking agreement with the old code is enough: atx. Plan for the legacy-code habits that remain after conversion&lt;/li&gt;
&lt;li&gt;If you need to add paths or keep a record of decisions: AIMF. Plan for the setup, the 3 h 33 min of this verification (the span between commits), and my 6 decisions&lt;/li&gt;
&lt;li&gt;If you already have an atx analysis: import it in AIMF's Phase 0a. In every case, re-measuring each path comes next
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    Q1{"Adding access paths?"} --&amp;gt;|Yes| Q3{"Already have an atx analysis?"}
    Q1 --&amp;gt;|No| Q2{"Need decision records?"}
    Q2 --&amp;gt;|No| A["atx: move to .NET 10 and check parity"]
    Q2 --&amp;gt;|Yes| Q3
    Q3 --&amp;gt;|Yes| C["AIMF, importing the atx analysis"]
    Q3 --&amp;gt;|No| B["AIMF"]
    A --&amp;gt; R["Re-measure each path"]
    B --&amp;gt; R
    C --&amp;gt; R&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The diagram above summarizes the table and the list.&lt;/p&gt;

&lt;p&gt;What you can decide: two questions, whether you are adding paths and whether you need decision records, tell you whether to start with atx or with AIMF. The differences in time and cost are single observations, so do not use them as the basis for choosing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost estimates and observations
&lt;/h2&gt;

&lt;p&gt;Per row, the hourly-rate estimate and the value I attributed from Cost Explorer differed by −1% to −3% (largest −3.06%). This difference checks whether the hourly rates in the estimate match the observed values; it is not a reconciliation with an invoice. The cost identified for the whole verification is $35.91. That figure does not deduct the AWS Transform Credit line (−$3.62); if the Credit is deducted, the AWS Transform portion nets to $0.&lt;/p&gt;

&lt;p&gt;The values in this chapter are either estimates (unit price times hours) or single observations from one environment. Cost Explorer values are before billing is final (&lt;code&gt;Estimated: true&lt;/code&gt;). They are not invoice amounts, and they do not guarantee that production would cost the same. I read them on 2026-10-13 JST with UnblendedCost, Credit and Refund excluded, for 2026-10-11 and 2026-10-12 (UTC dates). I read them again on 2026-10-21 JST under the same conditions: the main rows were unchanged to three decimals, and the values were still marked &lt;code&gt;Estimated: true&lt;/code&gt;. The account is shared with other work, so I did not use service-level totals. I used only the values filtered by the CloudFormation tag &lt;code&gt;aws:cloudformation:stack-name&lt;/code&gt; (&lt;code&gt;appmod-base&lt;/code&gt;, &lt;code&gt;appmod-stage3&lt;/code&gt;) and the values by resource ID.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Estimate&lt;/th&gt;
&lt;th&gt;Estimated duration&lt;/th&gt;
&lt;th&gt;Actual uptime and observed value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base environment (second build)&lt;/td&gt;
&lt;td&gt;$57.71&lt;/td&gt;
&lt;td&gt;72 hours&lt;/td&gt;
&lt;td&gt;About 33.6 hours (stack created 2026-10-11T02:08Z; deletion of &lt;code&gt;appmod-base&lt;/code&gt; finished about 11:44Z on 2026-10-12). The tag-attributed observed value is $21.18, excluding Directory Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base environment, Directory Service&lt;/td&gt;
&lt;td&gt;Hourly rate $0.146 × 33.6 hours ≈ $4.91&lt;/td&gt;
&lt;td&gt;33.6 hours&lt;/td&gt;
&lt;td&gt;Not separable by resource. The difference method (estimating this verification's share by subtracting the steady usage of days without the verification from the usage of the verification days) gives about $4.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base environment (first build)&lt;/td&gt;
&lt;td&gt;$19.23&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;td&gt;Tag-attributed $6.31 (2026-10-07). The uptime is unknown, so I did not compare it with the estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;atx analysis and dotnet-modernization&lt;/td&gt;
&lt;td&gt;Analysis about $3.24 (92.46 min × $0.035; upper bound $4.20). dotnet $0 inside the free allowance, about $4.87 outside (upper bound $10.50)&lt;/td&gt;
&lt;td&gt;About 15 min for analysis, about 20 min for dotnet&lt;/td&gt;
&lt;td&gt;Cost Explorer shows AWS Transform on 2026-10-11 as Usage $3.6221 (103.49 minutes: 92.465 for analysis, $3.24, and 11.025 for dotnet-modernization, $0.39) and a Credit of −$3.6221 for the same amount, so the net is $0. 2026-10-12 is $0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda (each stage 3 run)&lt;/td&gt;
&lt;td&gt;Upper bounds of $0.04 (s3), $0.01 (s3b), $0.01 (s3m)&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;$0 on both days when filtered by tag. I did not measure the actual request counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AIMF&lt;/td&gt;
&lt;td&gt;284.26 Kiro credits (limit 5,000)&lt;/td&gt;
&lt;td&gt;3 h 33 min from the first to the last commit&lt;/td&gt;
&lt;td&gt;As in the estimate column. Not converted to dollars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/reproduce.md#L767-L777" rel="noopener noreferrer"&gt;the estimate and actual uptime in docs/en/reproduce.md&lt;/a&gt; (commit-pinned; I reorganized the columns)&lt;/p&gt;

&lt;p&gt;The $57.71 estimate for 72 hours is not the cost of the roughly 33.6 hours the environment was up. The whole estimate prorated to 33.6 hours is about $26.9 (the breakdown is in the private estimate made at approval time).&lt;/p&gt;

&lt;h3&gt;
  
  
  Hourly-rate estimates against observed values
&lt;/h3&gt;

&lt;p&gt;I prorated the estimate's hourly rates to the same 33.6 hours of uptime and compared them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Estimated hourly rate × 33.6 hours (USD)&lt;/th&gt;
&lt;th&gt;Observed (USD)&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FSx for ONTAP (SSD 1,024 GiB + 128 MBps throughput)&lt;/td&gt;
&lt;td&gt;12.41&lt;/td&gt;
&lt;td&gt;12.03&lt;/td&gt;
&lt;td&gt;−3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 (Windows + Linux)&lt;/td&gt;
&lt;td&gt;6.41&lt;/td&gt;
&lt;td&gt;6.36&lt;/td&gt;
&lt;td&gt;−1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC interface endpoints&lt;/td&gt;
&lt;td&gt;2.82&lt;/td&gt;
&lt;td&gt;2.77&lt;/td&gt;
&lt;td&gt;−2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Managed Microsoft AD&lt;/td&gt;
&lt;td&gt;4.91&lt;/td&gt;
&lt;td&gt;4.80 (difference method, inference)&lt;/td&gt;
&lt;td&gt;−2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total (excluding AD; the observed value includes $0.02 of S3)&lt;/td&gt;
&lt;td&gt;21.64&lt;/td&gt;
&lt;td&gt;21.18&lt;/td&gt;
&lt;td&gt;−2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/environment.md#L203-L250" rel="noopener noreferrer"&gt;the tag-attribution table and the estimate-against-actual comparison in docs/en/environment.md&lt;/a&gt; (I renamed the column "Observed (tag)" to "Observed" and changed some row names)&lt;/p&gt;

&lt;p&gt;The observed value 21.18 is the sum of FSx for ONTAP 12.03, EC2 6.36, VPC 2.77, and S3 0.02. Each value is the two-day sum of the unrounded figures Cost Explorer returned, rounded afterwards. If you round each daily row first and add them up, some days are off by 0.01 (for 2026-10-11, the unrounded total is 13.8632 and the sum of the rounded rows is 13.87). The AD figure of 4.80 is an inference, so it is not in this total.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identified cost of the whole verification
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Day (UTC) and basis&lt;/th&gt;
&lt;th&gt;Amount (USD)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tag-attributed: first build&lt;/td&gt;
&lt;td&gt;2026-10-07&lt;/td&gt;
&lt;td&gt;6.31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag-attributed: second build&lt;/td&gt;
&lt;td&gt;2026-10-11, 2026-10-12&lt;/td&gt;
&lt;td&gt;21.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag-attributed subtotal&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;27.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directory Service, second build&lt;/td&gt;
&lt;td&gt;Difference-method inference&lt;/td&gt;
&lt;td&gt;4.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Transform usage&lt;/td&gt;
&lt;td&gt;Cost Explorer Usage line (Credit line excluded)&lt;/td&gt;
&lt;td&gt;3.62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identified cost&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;35.91&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns/blob/352311cc74b85897acde680482f18c990016b2a7/docs/en/environment.md#L252-L313" rel="noopener noreferrer"&gt;the AD difference method, the identified total, and the AWS Transform line in docs/en/environment.md&lt;/a&gt; (I merged two tables into one)&lt;/p&gt;

&lt;p&gt;Not in the $35.91:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first build's Directory Service: an estimate only, about $1.6&lt;/li&gt;
&lt;li&gt;Secrets Manager, CloudWatch, and Lambda: not separable; estimated at $0.1 to $0.2 in total&lt;/li&gt;
&lt;li&gt;The four secrets I created by accident (failure 8): expected to be about $0.015 (1.5 cents)&lt;/li&gt;
&lt;li&gt;The AWS Transform Credit line: with it included, the net is $0&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Costs that could not be attributed, and the teardown check
&lt;/h3&gt;

&lt;p&gt;This section answers two questions. The first is which costs of this verification I could not attribute, and why: AWS Directory Service, which Cost Explorer cannot filter by resource ID or by tag, and AWS Transform, whose billed minutes do not match the minutes in the atx records. The second is what I checked to confirm that everything created for the verification was destroyed. The cost values are Cost Explorer values before billing is final (Estimated), or inferences from them, and they are not invoice amounts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Costs that could not be attributed, and why
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Amount (USD; Cost Explorer values are Estimated)&lt;/th&gt;
&lt;th&gt;Attributed?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Source and tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS Directory Service (AWS Managed Microsoft AD, Standard, 2 DCs), second build&lt;/td&gt;
&lt;td&gt;About 4.80&lt;/td&gt;
&lt;td&gt;No. Inferred with the difference method&lt;/td&gt;
&lt;td&gt;Cost Explorer shows no resource ID (&lt;code&gt;NoResourceId&lt;/code&gt;), and the resource carries no CloudFormation tag&lt;/td&gt;
&lt;td&gt;Inference by the difference method (assumes the baseline is constant). Calculation in the next table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Directory Service, first build (2026-10-07)&lt;/td&gt;
&lt;td&gt;About 1.6&lt;/td&gt;
&lt;td&gt;No. The difference method cannot be used&lt;/td&gt;
&lt;td&gt;The usage on the day before, 2026-10-06, was 48.52 DC-hours, half of the baseline, so the baseline cannot be called constant&lt;/td&gt;
&lt;td&gt;Estimate only. Not in the $35.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Transform (2026-10-11)&lt;/td&gt;
&lt;td&gt;Usage line 3.6221; Credit line of the same day −3.6221&lt;/td&gt;
&lt;td&gt;The amount, yes (the service-level Usage line). The minutes, no match&lt;/td&gt;
&lt;td&gt;The Usage line shows 103.49 minutes. The atx records show 92.46 minutes for analysis and 139.00 for dotnet-modernization, 231.46 in total, so the gap is 127.97 minutes. All of the gap is in dotnet-modernization. A free allowance may be the cause, but I have not confirmed it (details below the table)&lt;/td&gt;
&lt;td&gt;Cost Explorer Usage and Credit lines (Estimated); atx records&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The identified cost of the whole verification, $35.91, includes the roughly $4.80 in the first row and the AWS Transform Usage line of $3.62. It does not include the roughly $1.6 in the second row. It does not deduct the AWS Transform Credit line (−$3.62); if the Credit is deducted, the AWS Transform portion nets to $0.&lt;/p&gt;

&lt;p&gt;I could explain part of the AWS Transform minute gap (2026-10-11). The match between the hourly Cost Explorer data (UTC) and the atx log (&lt;code&gt;debug.log&lt;/code&gt;) is verified (once each). Whether a free allowance is the cause is a hypothesis.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How the minutes are counted: the atx figures of 92.46 and 139.00 minutes are sums over the main agent and the sub-agents, not wall-clock time (verified). Wall-clock time was about 15 minutes for the analysis and about 20 minutes for dotnet-modernization. The pricing page says that agent minutes exceed wall-clock time when several agents run in parallel (documented). The analysis alone has 78.95 minutes on the main agent, which is already more than its wall-clock time of about 15 minutes, so I have not confirmed that parallelism by itself explains the difference.&lt;/li&gt;
&lt;li&gt;The analysis matches: the hourly Cost Explorer data (UTC) shows 63.318 minutes at 09:00 and 29.147 at 10:00, 92.465 in total. That equals the atx record for the same hours (63.318 and 29.147).&lt;/li&gt;
&lt;li&gt;Where the gap is: the Usage for the 11:00 UTC hour, when dotnet-modernization ran, is 11.025 minutes. The other 127.97 of the 139.00 minutes in the atx record do not appear as billable.&lt;/li&gt;
&lt;li&gt;Dates and unit: both atx runs (the analysis and dotnet-modernization) fall inside UTC 2026-10-11, so a date boundary is not the cause. The total from 1 September to 21 October is also 103.49 minutes. The unit is minutes (AgentMinute).&lt;/li&gt;
&lt;li&gt;A possible free allowance: AWS announced that the AWS-managed .NET modernization transformation includes 50,000 free agent minutes per month (&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-transform-dotnet-cli/" rel="noopener noreferrer"&gt;AWS: .NET modernization via CLI is generally available&lt;/a&gt;; the allowance's existence is documented). Most of the dotnet-modernization minutes may have fallen inside it, but Cost Explorer shows no line for the allowance and I could not see how much of it was used.&lt;/li&gt;
&lt;li&gt;Still unexplained: why exactly 11.025 minutes were billed. The seven PhaseValidator sub-agent runs (validation steps) in the same execution total 11.027 minutes, which is close to 11.025 minutes, but they differ by 0.002 minutes and chance cannot be ruled out. I also did not confirm why CloudWatch reports 144.75 minutes for dotnet-modernization against 139.00 in the atx record (a 5.75-minute difference).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference method is an inference that subtracts the steady usage of days without the verification from the usage of the verification days, to find this verification's share. The calculation for the second build's Directory Service is as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Calculation or basis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (mean of days without the verification)&lt;/td&gt;
&lt;td&gt;95.72 DC-hours per day&lt;/td&gt;
&lt;td&gt;Mean of 2026-10-08 to 2026-10-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difference on 2026-10-11&lt;/td&gt;
&lt;td&gt;41.40 DC-hours&lt;/td&gt;
&lt;td&gt;137.12 − 95.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difference on 2026-10-12&lt;/td&gt;
&lt;td&gt;24.36 DC-hours&lt;/td&gt;
&lt;td&gt;120.08 − 95.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total difference&lt;/td&gt;
&lt;td&gt;65.76 DC-hours&lt;/td&gt;
&lt;td&gt;2.1% off the expected 67.2 DC-hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit price&lt;/td&gt;
&lt;td&gt;0.073 USD per DC-hour&lt;/td&gt;
&lt;td&gt;7.012 USD ÷ 96.05 DC-hours on 2026-10-08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated amount&lt;/td&gt;
&lt;td&gt;About 4.80 USD&lt;/td&gt;
&lt;td&gt;65.76 × 0.073&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 0.073 USD per DC-hour matches the Price List unit price for &lt;code&gt;AWSDirectoryService&lt;/code&gt;, &lt;code&gt;APN1-Std-MicrosoftAD-DC-Usage&lt;/code&gt; (Tokyo, Standard); I read it on 2026-10-21.&lt;/p&gt;

&lt;p&gt;I read the atx unit price of $0.035 per agent minute again on 2026-10-21 in the Price List (&lt;code&gt;AWSTransform&lt;/code&gt;, &lt;code&gt;APN1-AgentMinute&lt;/code&gt;, Tokyo). It had not changed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Teardown check results
&lt;/h4&gt;

&lt;p&gt;I tore the base environment down on 2026-10-12. I confirmed the teardown through API listings and the teardown steps. The results are as follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;How it was checked&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stacks starting with &lt;code&gt;appmod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 buckets starting with &lt;code&gt;appmod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets starting with &lt;code&gt;appmod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda functions starting with &lt;code&gt;appmod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Access Points attachments starting with &lt;code&gt;appmod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler schedules starting with &lt;code&gt;appmod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log groups starting with &lt;code&gt;appmod&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File system, directory, EC2 instances, VPC&lt;/td&gt;
&lt;td&gt;API listing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NotFound&lt;/code&gt; or &lt;code&gt;terminated&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2026-10-13 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leftover FlexClones&lt;/td&gt;
&lt;td&gt;Environment teardown steps 4 and 5&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;2026-10-07 (first build), 2026-10-12 (second build)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;it_*&lt;/code&gt; Snapshots&lt;/td&gt;
&lt;td&gt;Environment teardown steps 4 and 5&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;2026-10-07 (first build), 2026-10-12 (second build)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery queue&lt;/td&gt;
&lt;td&gt;Environment teardown steps 4 and 5&lt;/td&gt;
&lt;td&gt;Empty&lt;/td&gt;
&lt;td&gt;2026-10-07 (first build), 2026-10-12 (second build)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily cost for 2026-10-13 (filtered by the tags &lt;code&gt;appmod-base&lt;/code&gt; and &lt;code&gt;appmod-stage3&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Check in Cost Explorer (daily, UnblendedCost)&lt;/td&gt;
&lt;td&gt;0 (&lt;code&gt;Estimated: true&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;2026-10-21 JST&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What you can decide: for a configuration where you know the uptime, you can use the unit-price-times-hours estimate as material for a budget draft, as a single environment's data point. Before you use it as the basis of a budget, check an invoice. The items to check are the three whose limits are written in this chapter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have not yet reconciled the figures with an invoice&lt;/li&gt;
&lt;li&gt;For the first build's Directory Service I have only an inference, not an actual amount&lt;/li&gt;
&lt;li&gt;The 127.97-minute gap in AWS Transform minutes is entirely in dotnet-modernization. A free allowance may be the cause, but I have not confirmed it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What has not been measured yet
&lt;/h2&gt;

&lt;p&gt;This chapter lists only the items that you need when deciding whether to apply this approach to your own app migration, and that I found measured nowhere in the documents of my other repositories that I checked. There are three. For each item I write what I did not measure, what another verification measured, when not knowing it hurts, and how to measure it. The results of other verifications are labeled carried-over. They are not values measured in this post's environment.&lt;/p&gt;

&lt;p&gt;These items are not in this chapter. Security styles &lt;code&gt;unix&lt;/code&gt; and &lt;code&gt;mixed&lt;/code&gt;, other ONTAP versions and Multi-AZ, reconciliation with an invoice, and performance are in "When this post fits and when it does not" and "What this post covers and what it does not". The limits of the cost figures are in "Cost estimates and observations". I left out NFS without name mapping, the case where the writability check finds a file not writable, NFS Kerberos and mount options, and pairs of the same OS. They are variants of this post's question, and I judged that they add nothing to your decision about migrating your own app.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;S3 Access Points data operations while the AD domain controller is unreachable

&lt;ul&gt;
&lt;li&gt;What I did not measure: the results of S3 Access Points data operations (ListObjectsV2, GetObject, PutObject) on an AD-joined SVM when the domain controller (DC) is unreachable. In this verification I used the access point with the DC reachable&lt;/li&gt;
&lt;li&gt;Status of public information (open): whether every data operation on an AD-joined SVM requires DC reachability is something I could not confirm, either from a record a reader can reproduce or from a public primary source (&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Adoption-Playbook/blob/8c38b513e90aee33dbb828acdd6b3c9743132fe9/docs/ja/domains/client-access/notes/what-an-endpoint-needs-to-reach-s3-access-points.md" rel="noopener noreferrer"&gt;What an endpoint needs to reach S3 Access Points&lt;/a&gt;, Japanese)&lt;/li&gt;
&lt;li&gt;What another verification measured (carried-over): on an SVM that was joined to a domain but found no DC at all, HeadBucket, ListObjectsV2, GetObject, PutObject, and DeleteObject all succeeded when the identity resolved to a local SMB user on the SVM (2026-08-26, ap-northeast-1, ONTAP 9.18.1P3D1, 128 MBps, an NTFS volume; &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/blob/1961fef256873d70a0d60c71d5697fd69a012765/docs/en/portal-identity-verification-results.md" rel="noopener noreferrer"&gt;file portal identity and visibility measurements&lt;/a&gt;). With a domain account as the identity, that verification could not tell whether a DC is required, because its environment had no reachable DC. That document says a re-measurement in an environment with a reachable DC is needed. This post's identity is the AD domain account &lt;code&gt;appsvc&lt;/code&gt; and its environment had a reachable DC, so the conditions are close to that re-measurement. But I did not cut off the DC in this post either&lt;/li&gt;
&lt;li&gt;When not knowing it hurts: you use S3 Access Points on an AD-joined SVM and AD goes down&lt;/li&gt;
&lt;li&gt;How to measure: cut the route to the DC with a positive control and take the result of each operation. Do not judge by a successful HeadBucket alone&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;When a file written by Lambda through S3 Access Points overlaps with a file a Windows app handles over SMB

&lt;ul&gt;
&lt;li&gt;What I did not measure: the results of two things. A write from S3 Access Points to a file while SMB holds an exclusive lock on it. The time until Windows can read a file written through S3 Access Points, and the time until S3 Access Points can read a file Windows wrote over SMB. In this verification Lambda ran only as the writer and there was no reader. The S3 API has no operation to open exclusively, and file-locking was skipped&lt;/li&gt;
&lt;li&gt;What another verification measured (carried-over): the median time until a 64 B object written through S3 Access Points could be read directly from the volume over NFS (NFSv3, &lt;code&gt;actimeo=0&lt;/code&gt;) was 3 ms, and until it could be read from the FlexCache side (FlexCache is an ONTAP volume that caches the data of a source volume) over a Linux cifs mount (SMB 3.0, &lt;code&gt;cache=none&lt;/code&gt;) it was 7 ms (concurrency 1, 30 runs, 2026-08-09 and 2026-08-10, ap-northeast-1, ONTAP 9.18.1P3D1, 128 MBps, UNIX security style, UNIX root identity; &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/cross-protocol-directions.md" rel="noopener noreferrer"&gt;comparison of visibility in all directions&lt;/a&gt;, Japanese). These are not values read by a Windows client, and not values for an NTFS volume&lt;/li&gt;
&lt;li&gt;When not knowing it hurts: a Lambda function and a Windows app handle the same file at the same time. Also when you must build into the design the wait until Windows can read a file Lambda wrote&lt;/li&gt;
&lt;li&gt;How to measure: design a pair experiment that links Lambda and Windows by a sync ID, and measure both directions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The five behaviors on a clone of an NTFS volume made with FlexClone

&lt;ul&gt;
&lt;li&gt;What I did not measure: the five behaviors of a clone of an NTFS volume made with FlexClone, opened over SMB, NFS, and S3 Access Points. I did not create a single FlexClone in this verification&lt;/li&gt;
&lt;li&gt;What another verification measured (carried-over): when I attached S3 Access Points to a clone of a 1 GiB volume with the UNIX security style, LIST, GET, and PUT worked. The sha256 of a 256 MiB file matched the source volume, and an object PUT to the clone did not appear in the source volume (2026-08-26, ap-northeast-1, ONTAP 9.18.1P3D1, 128 MBps, UNIX root identity; &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/91178f7f792ff9b4e26be60735e60d0f68aa511b/docs/ja/reference/limits/s3ap-interoperability.md" rel="noopener noreferrer"&gt;measurement of ONTAP features over S3 Access Points&lt;/a&gt;, Japanese). That document has no measurement of a clone of an NTFS volume&lt;/li&gt;
&lt;li&gt;When not knowing it hurts: you hand out a working volume per developer or per stage from a Snapshot of appdata and apply the results measured on appdata to the clone as they are&lt;/li&gt;
&lt;li&gt;How to measure: create a clone of an NTFS volume and run the same Probe over each path that opens the clone&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Governance note&lt;/strong&gt;&lt;br&gt;
This article describes design considerations. It is not a legal, compliance, or regulatory judgment. The ADR option summaries and the independent verification result are AI output and serve as supporting signals. I made the decisions, including the decision to keep the security style at ntfs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What you can decide: look at which of the three items above your environment falls under, and decide whether to add that item's "How to measure" to your own measurement items.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and choosing an access path
&lt;/h2&gt;

&lt;p&gt;The results are the three findings at the start of this post (the converted app ran on Lambda without moving the data, what changed with the path, and the difference in scope between atx and AIMF). There were 8 failures, and the measurement with no positive control invalidated the case-sensitivity values in 6 runs, more than any other failure.&lt;/p&gt;

&lt;p&gt;The three paths coexisted on the same volume up to stage 3. The criterion for choosing is which client you use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Constraints (from this environment's measurements)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stay on SMB&lt;/td&gt;
&lt;td&gt;You keep the existing Windows app unchanged and want to keep the case-insensitive assumption&lt;/td&gt;
&lt;td&gt;Windows and cifs resolved a reference with different letter case. NFS and S3 Access Points did not. Write visibility (the time until a file one client wrote becomes visible to the other) was 66 ms with Windows writing and cifs reading, and 7,968 ms in the reverse direction (s2m, one observation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add NFS&lt;/td&gt;
&lt;td&gt;You want a Linux app to run on the same data&lt;/td&gt;
&lt;td&gt;On NFS, a reference that differs only in letter case does not resolve. the NFS root user could not enter &lt;code&gt;/mnt/appdata&lt;/code&gt; (the cause is an inference from the export policy setting), while the uid 10001 user could read and write. I did not measure the case with no name mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serve through S3 Access Points&lt;/td&gt;
&lt;td&gt;An S3 API client such as Lambda needs to use the data&lt;/td&gt;
&lt;td&gt;On S3 Access Points, a reference that differs only in letter case does not resolve. The S3 API has no operation to open exclusively, and a key made only of &lt;code&gt;\&lt;/code&gt; was created directly under the volume root and appeared in the SMB listing as an 8.3 short name (one observation). The fixed NetworkOrigin and the AD domain controller reachability (open) are as in the table in the design chapter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The repository is here:&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns" rel="noopener noreferrer"&gt;https://github.com/Yoshiki0705/FSx-for-ONTAP-App-Modernization-Patterns&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The previous article is "&lt;a href="https://dev.to/aws-builders/aws-transform-now-supports-block-storage-migration-to-fsx-for-ontap-benefits-and-pitfalls-from-a-1hhe"&gt;AWS Transform Now Supports Block Storage Migration to FSx for ONTAP — Benefits and Pitfalls from a Hands-On EC2 Test&lt;/a&gt;".&lt;br&gt;
Related articles (two):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/designing-aws-modernization-with-vmware-migration-as-the-entry-point-why-fsx-for-ontap-as-the-3k24"&gt;Designing AWS Modernization with VMware Migration as the Entry Point — Why FSx for ONTAP as the Data Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/adding-a-file-portal-to-fsx-for-ontap-s3-access-points-choosing-between-amplify-gen2-and-887"&gt;Reaching an FSx for ONTAP NAS from a browser through an S3 Access Point — Amplify Gen 2 or Nextcloud (File Portal Part 1)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I thank the people who publish AIMF and atx, and the people who support my day-to-day work.&lt;/p&gt;

&lt;p&gt;I hope this is useful to anyone considering moving a .NET Framework app to a newer .NET without moving its data.&lt;/p&gt;

&lt;p&gt;See you next time.&lt;br&gt;
Bye now!!&lt;br&gt;
Yoshiki Fujiwara&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/aws-samples/sample-ai-modernization-flow" rel="noopener noreferrer"&gt;AIMF (pinned to &lt;code&gt;v0.11.0&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/jp/blogs/news/aidm-introducing-ai-modernization-flow/" rel="noopener noreferrer"&gt;AIMF introduction blog (Japanese)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.netapp.com/us-en/ontap/nfs-admin/security-styles-their-effects-concept.html" rel="noopener noreferrer"&gt;NetApp explanation of security styles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Adoption-Playbook/blob/8c38b513e90aee33dbb828acdd6b3c9743132fe9/docs/ja/ai-agents/ai-modernization-flow/README.md" rel="noopener noreferrer"&gt;Where AI Modernization Flow fits (FSx for ONTAP Adoption Playbook)&lt;/a&gt; (Japanese) — AIMF owns the app side, the Adoption Playbook owns the data side; the linked document cites this article's field results&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Adoption-Playbook/blob/8c38b513e90aee33dbb828acdd6b3c9743132fe9/docs/ja/ai-agents/ai-modernization-flow/aux-pack/README.md" rel="noopener noreferrer"&gt;FSx for ONTAP auxiliary pack&lt;/a&gt; (Japanese) — a drop-in pack for an AIMF migration project, surfacing storage-origin silent failures as Phase 0b–3 analysis points&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>dotnet</category>
      <category>serverless</category>
      <category>modernization</category>
    </item>
    <item>
      <title>Can the QoS burst defaults on FSx for ONTAP be changed from ONTAP 9.19.1, the same as on-premises? I checked on a live file system</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Tue, 20 Oct 2026 11:04:52 +0000</pubDate>
      <link>https://dev.to/aws-builders/can-the-qos-burst-defaults-on-fsx-for-ontap-be-changed-from-ontap-9191-the-same-as-on-premises-2ce7</link>
      <guid>https://dev.to/aws-builders/can-the-qos-burst-defaults-on-fsx-for-ontap-be-changed-from-ontap-9191-the-same-as-on-premises-2ce7</guid>
      <description>&lt;p&gt;Conclusion first: on Amazon FSx for NetApp ONTAP you can change the QoS burst defaults. Not only is the configuration accepted — a brief overshoot above the ceiling (the burst) happened exactly as configured. The catch: you cannot change this setting from the AWS Management Console or the AWS CLI (the AWS API). FSx for ONTAP has two ways in — the AWS-side consoles and APIs, and a separate path that drives the storage engine (ONTAP) directly through its own REST API / CLI. The QoS burst can be set only from that second path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this post covers, and what it does not
&lt;/h2&gt;

&lt;p&gt;A colleague at NetApp asked me, "since ONTAP 9.19.1 you can change the QoS burst defaults (burst-percent / burst-duration / burst-iops), but is that possible on FSx for ONTAP too?" — so I tried it.&lt;/p&gt;

&lt;p&gt;This post records what I measured on a verification FSx for ONTAP file system in ap-northeast-1 (ONTAP 9.19.1P2): setting the options, and confirming that a burst above the ceiling occurs as configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who benefits from this setting
&lt;/h2&gt;

&lt;p&gt;When several workloads or tenants share one file system, one workload can eat the bandwidth and slow the others — the "noisy neighbor" problem. A QoS ceiling holds that down, and burst is the buffer that lets a workload briefly exceed the ceiling at startup or cutover. Being able to change the burst defaults on 9.19.1 means you can tune &lt;em&gt;how&lt;/em&gt; the ceiling is exceeded per use case.&lt;/p&gt;

&lt;p&gt;On AWS, here is who it helps. If you run databases like Oracle / SQL Server / SAP on Amazon EC2 with their data on FSx for ONTAP, you can cap the nightly batch and reporting side to leave bandwidth for daytime transaction processing (OLTP), while absorbing the database-startup spike with burst. If you operate virtual desktops (VDI, where employees' Windows sessions run on servers rather than their own PCs), you can absorb the morning login storm with burst and keep fairness with a per-group ceiling. If you run multi-tenant SaaS on one file system, you can set ceilings at each level of the storage hierarchy (SVM → volume → qtree, a set of nested units) and vary the burst per tenant to express different service levels (SLA tiers). Each is the same move: keep the ceiling itself fixed, change only how it is exceeded, per tenant or workload.&lt;/p&gt;

&lt;p&gt;The use-case mappings and how to choose between QoS and the alternatives (application-layer rate limiting, separate file systems, separate throughput capacity) are organized in the companion note on GitHub (&lt;a href="https://github.com/Yoshiki0705/fsxn-adoption-playbook/blob/main/docs/en/domains/performance/notes/qos-ceiling-burst-use-cases.md" rel="noopener noreferrer"&gt;Which workloads the QoS ceiling and burst help&lt;/a&gt;). What follows is the premise behind all of that: whether FSx for ONTAP even lets you change the defaults.&lt;/p&gt;

&lt;p&gt;What it does not cover: whether a downgrade (revert) is operationally possible on FSx for ONTAP (unverified); the effect of burst values on overall workload cost; interaction with other features such as SnapMirror.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed from the previous post
&lt;/h2&gt;

&lt;p&gt;Earlier I measured the speed and monthly cost of using S3 as files versus FSx for ONTAP (&lt;a href="https://hakobiya.hatenablog.com/entry/fsxn-s3files-value-measured" rel="noopener noreferrer"&gt;I measured the speed and monthly cost of Amazon S3 Files&lt;/a&gt;). That time I measured the one-client speed limit and how it scales with client count. This time it is the same FSx for ONTAP, but the question is whether the defaults for &lt;em&gt;how&lt;/em&gt; a QoS ceiling is exceeded (burst) can be changed on 9.19.1. Not the ceiling itself, but the design lever for how it is exceeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What QoS burst is
&lt;/h2&gt;

&lt;p&gt;A QoS throughput ceiling caps a workload's throughput at a maximum (in IOPS — operations per second — or MB/s). Even when other workloads are competing for the same storage, it guarantees the workload does not exceed the cap. It is used to hold down a noisy neighbor.&lt;/p&gt;

&lt;p&gt;But the ceiling is not a hard wall: to respond to short spikes, by default a workload may exceed the ceiling by 50% for 1 second. This absorbs temporary resource demands — starting a database, booting many VMs — without permanently adding capacity.&lt;/p&gt;

&lt;p&gt;Since ONTAP 9.19.1, you can change this "50% / 1 second" default. There are three options.&lt;/p&gt;

&lt;p&gt;When I measured it, with the ceiling at 2000 IOPS, the default (burst-percent 50 / duration 1) reached about 3000 IOPS (+50%) for the first second only and settled to 2000 by the second second. Setting burst-percent to 25 shrank the first peak to about 2500 (+25%), and setting burst-duration to 5 held about 3000 for 5 seconds. The width, duration, and absolute magnitude of the excess moved exactly as configured.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;xychart-beta
    title "Burst behavior at ceiling=2000 IOPS (measured, 4k random read)"
    x-axis "seconds" [1, 2, 3, 4, 5, 6]
    y-axis "IOPS" 0 --&amp;gt; 3500
    line "default 50% / 1s" [3017, 2000, 2000, 2000, 2000, 2000]
    line "25% / 1s" [2507, 2000, 2000, 2000, 2000, 2000]
    line "50% / 5s" [3038, 2970, 3032, 2997, 2970, 2000]&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;burst-percent&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;the percentage over the ceiling (the peak excess width)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;burst-duration&lt;/td&gt;
&lt;td&gt;1 second&lt;/td&gt;
&lt;td&gt;how many seconds the excess is sustained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;burst-iops&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;the absolute IOPS cap during the excess (0 means unused)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;burst-percent and burst-iops are mutually exclusive: you set the excess either as a percentage or as an absolute IOPS value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking on a live file system
&lt;/h2&gt;

&lt;p&gt;I reached a verification FSx for ONTAP file system in ap-northeast-1 (ONTAP 9.19.1P2, single-AZ) from a Linux client inside the VPC, using the commands and API that drive the storage engine (ONTAP) directly. That management connection is only reachable from inside the VPC, so I went through a bastion host placed in the VPC.&lt;/p&gt;

&lt;p&gt;First, does the configuration go through? I created a QoS policy group with a 2000 IOPS ceiling, set burst-percent to 25, set burst-duration to 5. Both create and modify succeeded, and reading the values back confirmed them.&lt;/p&gt;

&lt;p&gt;Then the real question: does changing the setting actually change the read/write behavior? With a load-testing tool (fio) I drove 4 KB random reads well past the 2000 IOPS ceiling (without QoS it does 24,400 IOPS) and observed IOPS at one-second resolution. Burst only kicks in once enough "credit" has built up, so I left an idle period before the load to accumulate it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;t=1s peak&lt;/th&gt;
&lt;th&gt;Steady&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default (percent=50, duration=1)&lt;/td&gt;
&lt;td&gt;3017&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;exceeded ceiling by +50% for 1 second, settled to 2000 by the 2nd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;percent=25, duration=1&lt;/td&gt;
&lt;td&gt;2507&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;the peak excess shrank to +25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;percent=50, duration=5&lt;/td&gt;
&lt;td&gt;~3000 held for 5s&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;the excess lasted 5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;burst-iops=3000, duration=5&lt;/td&gt;
&lt;td&gt;~3000 held for 5s&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;the excess IOPS pinned to the absolute value 3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;disabled (0/0/0)&lt;/td&gt;
&lt;td&gt;2020 (~+1%)&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;the excess all but disappeared&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;burst-percent governed the peak excess width, burst-duration the number of seconds the excess lasted, and burst-iops the absolute IOPS during the excess. Setting 0/0/0 removes the burst entirely. Which aspect of the behavior moves is cleanly separated by which option you set.&lt;/p&gt;

&lt;p&gt;The NetApp documentation states that a ceiling may be exceeded by up to 10%, and by up to 50% to handle bursts. The default +50% peak and the ~+1% when disabled match that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The errors along the way were not FSx constraints
&lt;/h2&gt;

&lt;p&gt;Two errors came up, and both are ONTAP's own constraints, not something FSx for ONTAP blocks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;burst-iops and burst-percent cannot be specified together — they are mutually exclusive. Reset one to 0, then set the other, and it goes through.&lt;/li&gt;
&lt;li&gt;burst-iops must be larger than the ceiling IOPS. Against a 2000 IOPS ceiling, burst-iops=500 was rejected and 3000 was accepted. The command reference also says that if burst-iops is below max-throughput, max-throughput is used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separating "FSx for ONTAP cannot do this" from "this is how ONTAP behaves" matters. A managed service can hide or restrict settings the underlying product exposes, so when something is rejected, unless you isolate which layer rejected it, you misattribute the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which way in lets you change it, and which does not
&lt;/h2&gt;

&lt;p&gt;I only checked via the storage engine's own commands and API (ONTAP REST / CLI). On that path the burst defaults can be changed, and the change shows up in real read/write behavior.&lt;/p&gt;

&lt;p&gt;The AWS Management Console and the AWS CLI (the AWS API) have no item for these burst options. As noted at the top, FSx for ONTAP has two ways in — the AWS-side consoles and APIs, and the path that drives the storage engine (ONTAP) directly — and QoS settings live on the second one. So the practical takeaway: looking in the usual AWS console will not find it; you set it on the storage-engine side.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b6kzq1g2w3yw9dvr0qa.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b6kzq1g2w3yw9dvr0qa.png" alt="The burst setting has two ways in. The AWS-side consoles and APIs have no matching item; you set it from the storage engine (ONTAP) side" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture and operational caveats
&lt;/h2&gt;

&lt;p&gt;The intro covered who benefits. Here is the same setting as one picture of how you actually run it on AWS, with the points that trip you up in operation. The figure shows the flow from the workload to FSx for ONTAP, plus monitoring and automated configuration.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftk3e1sagx15lom7ka1n8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftk3e1sagx15lom7ka1n8.png" alt="The AWS integration patterns where the QoS ceiling and burst apply" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As the figure shows, setting QoS itself rides the path that drives the storage engine (ONTAP) — in the diagram, called from AWS Lambda / Amazon EventBridge. Setting the ceiling and burst by hand every time you add a tenant is not realistic, so you automate it as part of provisioning (the process that stands up each new environment). Per the two-ways-in split from earlier, this path is the storage-engine side; it is not on the AWS API side.&lt;/p&gt;

&lt;p&gt;Four caveats I hit, or expect to hit, with this shape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. There is no burst-specific metric to monitor.&lt;/strong&gt; What Amazon CloudWatch gives you is the throughput time series; there is no value that directly says "bursting now." Whether you are pinned at the ceiling or briefly exceeded it is something you read off the per-second series yourself. In this verification, the burst peak was invisible at anything coarser than one-second resolution — it averages away.&lt;/p&gt;

&lt;p&gt;The finest unit the standard CloudWatch metrics can target is the volume (&lt;code&gt;VolumeId&lt;/code&gt;), with the file system (&lt;code&gt;FileSystemId&lt;/code&gt;) above it; there is no breakdown (dimension) that identifies a QoS policy group or workload, so from the standard metrics you cannot tell which policy group a number belongs to. If the unit you want to monitor is the volume or the file system, the standard metrics are enough — but when you want it per policy group or per workload, you close that gap yourself. ONTAP has a facility that exposes performance figures (its counter manager), reachable over the REST API, and a small Lambda can read it on a schedule and write per-workload &lt;code&gt;FSxONTAP/QoS&lt;/code&gt; metrics into CloudWatch. I added exactly that collector to the &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Observability-integrations/blob/main/docs/en/monitoring-design.md" rel="noopener noreferrer"&gt;Observability-integrations repo&lt;/a&gt; and verified the collection mechanism against the same live 9.19.1P2 file system. Two honest limits stay. First, this facility returns running totals (numbers that only ever grow since boot), so a single read does not tell you "how many per second"; the collector reads twice in one run and divides the difference by the elapsed time to get a per-second value (an average over a short window, not a long one). Second, there is still no burst-specific figure, so these metrics show effective throughput and IOPS and "pinned at the ceiling" — "currently bursting" remains inferred, not measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. When ceilings are nested, the real overshoot is bounded by the tightest one.&lt;/strong&gt; For multi-tenant SaaS where you set ceilings at each level of the storage hierarchy (SVM → volume → qtree), wherever you set burst, it only works up to the strictest ceiling. A generous burst on a lower-level qtree still tops out at a low SVM ceiling above it. Unless you manage which value sits at which layer consistently, you do not get the service-level (SLA) tiers you intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ingest paths (AWS DataSync / SnapMirror) can crowd out production reads.&lt;/strong&gt; Capping the migration/replication ingest keeps it from eating production reads at peak. One pattern is to relax it with burst only during catch-up windows. This is a design expectation, though — I did not measure specific burst values for ingest workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. ONTAP QoS burst and FSx-for-ONTAP-side burst are different layers.&lt;/strong&gt; FSx for ONTAP itself also bursts above a baseline for a time (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/performance.html" rel="noopener noreferrer"&gt;performance documentation&lt;/a&gt;). QoS burst governs "how far a workload may exceed its ceiling"; FSx-side burst governs "how much the file system itself can deliver" — different layers. In this measurement the peak was ~3,000 IOPS (~12 MB/s) against ~24,400 IOPS available without QoS, a large margin, so FSx-side bursting did not affect the result (this part I confirmed by measurement). As a design caution, though: if you size so the sum of per-workload (ceiling + burst) exceeds the file system's baseline (sustained) performance, a workload might not even reach its ceiling once FSx-side credits are exhausted, and the same QoS settings can behave differently depending on credit state (I did not measure this saturated case). Size the sum of ceilings to the baseline performance, not the burst.&lt;/p&gt;

&lt;p&gt;Caveats 2 and 3, and the per-workload mappings from the intro, are cases where each service's role is real but the combination with QoS is my design expectation. What I measured is the first half: that the defaults can be changed and are reflected in real I/O. The detailed use-case mappings are in the &lt;a href="https://github.com/Yoshiki0705/fsxn-adoption-playbook/blob/main/docs/en/domains/performance/notes/qos-ceiling-burst-use-cases.md" rel="noopener noreferrer"&gt;GitHub note&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  QoS is not the only way to cap throughput
&lt;/h2&gt;

&lt;p&gt;Isolating workloads and capping throughput is not QoS alone. You can rate-limit at the application layer, separate the file systems themselves, or allocate throughput capacity separately — each with its own fit.&lt;/p&gt;

&lt;p&gt;Roughly, it splits on "is the storage layer enough, or do you need to see the meaning of the requests?" If you want to control by request type or priority, choose the application layer; if you only want to suppress storage contention, choose QoS. If you need full physical isolation between tenants, separate file systems; if you need guaranteed bandwidth, separate capacity. QoS is not mutually exclusive with these — they can be combined. None is always right; you pick by requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On FSx for ONTAP 9.19.1P2 the QoS burst defaults can be changed. Confirmed in both configuration and real read/write behavior.&lt;/li&gt;
&lt;li&gt;You change it from the storage-engine (ONTAP) side. The AWS Management Console and AWS CLI (the AWS API) have no matching item.&lt;/li&gt;
&lt;li&gt;burst-percent governs the peak excess width, burst-duration the sustained seconds, burst-iops the absolute IOPS during the excess.&lt;/li&gt;
&lt;li&gt;The mutual exclusion and the value floor are ONTAP's own constraints, not ones FSx for ONTAP adds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the measured values are on GitHub with their environment (&lt;a href="https://github.com/Yoshiki0705/fsxn-adoption-playbook" rel="noopener noreferrer"&gt;the FSx for ONTAP adoption playbook&lt;/a&gt;, performance domain). I hope this helps anyone who has set a QoS ceiling and wants to shape how it is exceeded to their own requirements.&lt;/p&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

&lt;p&gt;Yoshiki Fujiwara&lt;/p&gt;

</description>
      <category>aws</category>
      <category>fsx</category>
      <category>storage</category>
      <category>ontap</category>
    </item>
    <item>
      <title>Seeing and restoring a RAM-shared logically air-gapped vault from another account and Region — measured</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Tue, 20 Oct 2026 10:17:27 +0000</pubDate>
      <link>https://dev.to/aws-builders/seeing-and-restoring-a-ram-shared-logically-air-gapped-vault-from-another-account-and-region--2jbe</link>
      <guid>https://dev.to/aws-builders/seeing-and-restoring-a-ram-shared-logically-air-gapped-vault-from-another-account-and-region--2jbe</guid>
      <description>&lt;p&gt;The earlier post copied an Amazon FSx for NetApp ONTAP (hereafter FSx for ONTAP) backup from Tokyo to Osaka and restored it, all inside one account. In September 2026, &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-backup-air-gapped-vault-fsx-ontap/" rel="noopener noreferrer"&gt;AWS Backup added logically air-gapped vault support for FSx for ONTAP&lt;/a&gt;, and trying that is what prompted this post. It picks up where the earlier one stopped: &lt;strong&gt;an AWS Backup logically air-gapped (LAG) vault owned by another account, shared over AWS RAM so the recipient can see it, and restored into the recipient's own file system&lt;/strong&gt;. The earlier post was a same-account cross-Region copy. This one is about &lt;strong&gt;visibility and restore across the account boundary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A LAG vault owned by one account becomes visible to another account over AWS RAM&lt;/strong&gt; (V1). The recipient's &lt;code&gt;--by-shared&lt;/code&gt; lists exactly one vault, its ARN account is the owner, and &lt;code&gt;VaultType&lt;/code&gt; is &lt;code&gt;LOGICALLY_AIR_GAPPED_BACKUP_VAULT&lt;/code&gt;. It is visible in &lt;strong&gt;both&lt;/strong&gt; Tokyo (&lt;code&gt;ap-northeast-1&lt;/code&gt;) and Osaka (&lt;code&gt;ap-northeast-3&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The share is one-directional.&lt;/strong&gt; The owner's own &lt;code&gt;--by-shared&lt;/code&gt; is empty (V2) — shared-with-me is not shared-by-me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The recipient must name the owner's account.&lt;/strong&gt; A &lt;code&gt;list-recovery-points&lt;/code&gt; call without &lt;code&gt;--backup-vault-account-id&lt;/code&gt; is &lt;code&gt;AccessDeniedException&lt;/code&gt;; with it, it succeeds and returns one recovery point (V4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The portal visualizes, the CLI restores.&lt;/strong&gt; With the shared-vault ARN in its IAM policy the portal shows the air-gap chip and the owner account (V3, V5), but the restore dialog's confirm button stays &lt;strong&gt;disabled&lt;/strong&gt; — the portal does not start the restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoking the share drops the vault within 1–2 seconds&lt;/strong&gt; in both Regions (V6); later calls return &lt;code&gt;AccessDeniedException&lt;/code&gt;. An already-restored volume is untouched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The shared recovery point restored into the recipient's existing file system&lt;/strong&gt;, job &lt;code&gt;COMPLETED&lt;/code&gt; in 1362 s (~22.7 min), the restored volume reached &lt;code&gt;CREATED&lt;/code&gt; (1 GiB, junction &lt;code&gt;/vol_restored&lt;/code&gt;) (V7).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every figure is &lt;strong&gt;n=1, one environment, one run&lt;/strong&gt;: 2026-10-12 (JST), Tokyo primary + Osaka secondary, &lt;code&gt;SINGLE_AZ_1&lt;/code&gt; 128 MBps 1024 GiB, aws-cli 2.36.5. Not a generalization to another environment or scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope&lt;/strong&gt;: visibility of a shared LAG vault from a recipient account across two Regions (V1, V2, V4), the portal's read path (V3, V5), revocation (V6), and a restore from the shared recovery point into the recipient's existing file system (V7).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out of scope&lt;/strong&gt;: multi-party approval (MPA — design only, not run, because it needs two AWS Organizations and a 3–20 person approval team that a single-org two-account lab cannot provide); the organization-sharing path (E13 — this environment used the invitation path with organization sharing off); a cross-Region restore into a recipient file system in that Region (E6 — this run kept to visibility and the same-path restore).&lt;/p&gt;




&lt;h2&gt;
  
  
  Shape of the setup
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Owner account&lt;/th&gt;
&lt;th&gt;Recipient account&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vault&lt;/td&gt;
&lt;td&gt;LAG vault (compliance-mode lock, 1-week retention) + the RAM share&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visibility&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--by-shared&lt;/code&gt; is empty (shares out, does not share in)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--by-shared&lt;/code&gt; lists the owner's one LAG vault&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;restores the shared recovery point into an &lt;strong&gt;existing&lt;/strong&gt; file system → new volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revocation&lt;/td&gt;
&lt;td&gt;revokes the RAM share&lt;/td&gt;
&lt;td&gt;the vault leaves &lt;code&gt;--by-shared&lt;/code&gt; in 1–2 s; the restored volume stays&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The point is that the vault lives in the owner account and the recipient only sees and restores it.&lt;/strong&gt; The owner holds the air-gapped copy; the recipient never owns the vault, only the volume it restores.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On vendor neutrality&lt;/strong&gt;: this post evaluates the shared-LAG-vault path on technical grounds only. SnapMirror and the backup-copy / shared-vault paths each suit a different context, and neither is framed as better than the other. The recommended option's own constraints are stated alongside the others.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You want a tamper-resistant retention layer in a &lt;strong&gt;separate account&lt;/strong&gt; from production&lt;/li&gt;
&lt;li&gt;You want backup copies to land outside your own account&lt;/li&gt;
&lt;li&gt;You are isolating the owning and the restoring account against ransomware or credential compromise&lt;/li&gt;
&lt;li&gt;You want to know which operations the portal handles and which the CLI handles&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What changed from the backup-copies post — where it is &lt;em&gt;seen from&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;The earlier post was about where a backup can live: both source and destination inside one account. This post is about an owner's LAG vault being &lt;strong&gt;shared to another account, and the recipient seeing it and restoring it&lt;/strong&gt; — across the account boundary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Earlier post&lt;/th&gt;
&lt;th&gt;This post&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What moved&lt;/td&gt;
&lt;td&gt;a backup, cross-Region, same account&lt;/td&gt;
&lt;td&gt;a vault, made visible cross-account over RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who puts&lt;/td&gt;
&lt;td&gt;the owner (same account as the restore)&lt;/td&gt;
&lt;td&gt;the owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who sees / restores&lt;/td&gt;
&lt;td&gt;the same account&lt;/td&gt;
&lt;td&gt;a different account (the recipient)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direction&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;owner → recipient, one-way; revoke is the owner's call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Blur the account boundary and the runbook comes out wrong: the recipient cannot list recovery points without naming the owner account, and the owner can revoke visibility at any time while the recipient's restored volume stays put.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;2026-10-12 (JST)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;One per item (n=1).&lt;/strong&gt; Variance not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regions&lt;/td&gt;
&lt;td&gt;Primary &lt;code&gt;ap-northeast-1&lt;/code&gt; (Tokyo), second Region &lt;code&gt;ap-northeast-3&lt;/code&gt; (Osaka)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SINGLE_AZ_1&lt;/code&gt;, 128 MBps, 1024 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws-cli/2.36.5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accounts&lt;/td&gt;
&lt;td&gt;Single organization, two accounts (owner and recipient). Organization sharing off; shared via the invitation path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vault type&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LOGICALLY_AIR_GAPPED_BACKUP_VAULT&lt;/code&gt;, compliance-mode lock, 1-week retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;The lab's own scripts and templates, plus the deployed portal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portal dummy account&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;111122223333&lt;/code&gt; (account IDs in the screenshots are masked)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Do not use these n=1 durations as an RTO basis for your environment.&lt;/strong&gt; They are measured against a 1 GiB volume with one recovery point, where fixed overhead dominates.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I measured (V1–V7)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One shared vault, seen from the recipient (V1, V2)
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;--by-shared&lt;/code&gt; in the recipient account returned exactly one LAG vault shared by the owner. Its ARN account is the owner, and &lt;code&gt;VaultType&lt;/code&gt; is &lt;code&gt;LOGICALLY_AIR_GAPPED_BACKUP_VAULT&lt;/code&gt;. 18 s.&lt;/p&gt;

&lt;p&gt;Two quirks of the list: the by-shared row has &lt;strong&gt;no &lt;code&gt;VaultState&lt;/code&gt;&lt;/strong&gt; and shows &lt;code&gt;NumberOfRecoveryPoints&lt;/code&gt; &lt;strong&gt;0&lt;/strong&gt;. The count populates on the vault detail, not on the list.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcq4dkh8j1tfr8djxylgp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcq4dkh8j1tfr8djxylgp.png" alt="The recipient's shared-vault list (via RAM) showing one LAG vault" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recipient's detail view shows the vault type (logically air-gapped), the compliance-mode lock, a one-week retention, and one recovery point.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7evwbyd7pz3x4ywoivyi.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7evwbyd7pz3x4ywoivyi.png" alt="The recipient console's shared-vault detail: air-gapped type, locked, one-week retention, one recovery point" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same shared vault and recovery point were visible to the recipient in Osaka, the second Region. The share holds in both Regions.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fui6ljbmbs6p4qcx4fibb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fui6ljbmbs6p4qcx4fibb.png" alt="The shared vault and recovery point visible to the recipient in the Osaka Region" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The direction checks out too (V2): the owner's own &lt;code&gt;--by-shared&lt;/code&gt; is empty in both Regions. Shared-with-me and shared-by-me are different lists, and this vault appears for the recipient, not the owner.&lt;/p&gt;

&lt;h3&gt;
  
  
  The owner side shows a shared status (V5)
&lt;/h3&gt;

&lt;p&gt;Seen from the owner, the same vault carries a "shared" status and the RAM-share row to the recipient account.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9y6btwtucrwiu2v5f4n.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9y6btwtucrwiu2v5f4n.png" alt="The owner console's LAG vault detail: shared status and the RAM-share row" width="800" height="684"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The copy jobs — from the standard vault into the Tokyo and Osaka LAG vaults — both completed. Which Region's list a cross-Region copy job lands in is in a later section.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi98j2ggl8x4e1dvpx02h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi98j2ggl8x4e1dvpx02h.png" alt="The owner console's copy jobs: two completed into the Tokyo and Osaka LAG-vault destinations" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A call without the account ID is denied (V4)
&lt;/h3&gt;

&lt;p&gt;When the recipient lists recovery points without &lt;code&gt;--backup-vault-account-id&lt;/code&gt;, it is &lt;code&gt;AccessDeniedException&lt;/code&gt;. With the owner's account ID it succeeds and returns one recovery point. The ARN shape is &lt;code&gt;arn:aws:backup:&amp;lt;region&amp;gt;:&amp;lt;owner&amp;gt;:recovery-point:&amp;lt;id&amp;gt;&lt;/code&gt;. Being shared is not the same as being readable in the recipient's plain context — the owner account has to be named. 18 s.&lt;/p&gt;

&lt;h3&gt;
  
  
  The portal visualizes; it does not start the restore (V3, V5)
&lt;/h3&gt;

&lt;p&gt;Tested with the shared-vault ARN added to the deployed portal's role. With it added, &lt;code&gt;listSharedBackupVaults&lt;/code&gt; succeeds without an error and the screen shows the air-gap chip and the owner account.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap9w40oib06d5mq6y4nb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap9w40oib06d5mq6y4nb.png" alt="The portal recovery-points screen: shared-vault choice, air-gapped chip, owner account shown" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the design line. The restore dialog opens, but its confirm button stays &lt;strong&gt;disabled&lt;/strong&gt;, and the portal does not initiate the restore. The restore is the job of the CLI script (08); the portal handles visualization and selection. The arn-present spec passed both of its tests. 41 s.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uf3eg62qbc8iw69sbtx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uf3eg62qbc8iw69sbtx.png" alt="The portal restore dialog with the confirm button disabled" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Revoking the share drops it in 1–2 seconds (V6)
&lt;/h3&gt;

&lt;p&gt;Revoking the share dropped the vault from the recipient's &lt;code&gt;--by-shared&lt;/code&gt; list in about 1–2 seconds in both Regions. Later &lt;code&gt;list-recovery-points&lt;/code&gt; calls return &lt;code&gt;AccessDeniedException&lt;/code&gt;. A volume already restored before the revoke is unaffected — visibility and the data materialized on the recipient side are different things. 2 s.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdw0z5r3hpwi036bufiz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdw0z5r3hpwi036bufiz.png" alt="After the revoke, the recipient's shared-vault list is empty" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Restore from the shared vault into the recipient's existing file system (V7)
&lt;/h3&gt;

&lt;p&gt;The shared vault's recovery point restored into the recipient's existing file system. The job reached &lt;code&gt;COMPLETED&lt;/code&gt; in 1362 s (~22.7 min). The restored volume &lt;code&gt;vol_restored&lt;/code&gt; reached &lt;code&gt;CREATED&lt;/code&gt; — 1 GiB, junction &lt;code&gt;/vol_restored&lt;/code&gt;. The restore role was accepted and the &lt;code&gt;Project&lt;/code&gt; tag was applied.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jrde7w74wlnt8e4cncf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jrde7w74wlnt8e4cncf.png" alt="The restored volume vol_restored in the recipient console (created, 1 GiB, junction /vol_restored)" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the full path holds: a LAG vault owned by another account, seen by the recipient, and restored on the recipient side.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constraints and out-of-scope
&lt;/h2&gt;

&lt;p&gt;Stated as scope, not as defects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MPA is design-only.&lt;/strong&gt; The template ships an &lt;code&gt;MpaApprovalTeamArn&lt;/code&gt; parameter (default empty), the docs mark it unverified, and it is tracked as a follow-up issue. It is not run here because MPA needs two AWS Organizations and a 3–20 person approval team, which a single-org two-account lab cannot satisfy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The organization-sharing path (E13) is not recorded&lt;/strong&gt;, because this environment used the invitation path with organization sharing off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cross-Region restore into a recipient file system in that Region (E6) was not run&lt;/strong&gt; — this run kept to visibility and the same-path restore.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where a cross-Region copy job appears
&lt;/h2&gt;

&lt;p&gt;One manual observation, separate from the scripted V-items (observed once). The cross-Region copy job (Tokyo source → Osaka LAG vault) appeared in the &lt;strong&gt;source&lt;/strong&gt; Region's &lt;code&gt;list-copy-jobs&lt;/code&gt;, carrying the destination vault ARN in &lt;code&gt;ap-northeast-3&lt;/code&gt;. It did &lt;strong&gt;not&lt;/strong&gt; appear in the destination Region's list. If you watch only the destination side for progress, you will not find it — that is the operational catch. Both copies' recovery points share the same delete date (source-backup creation time + 7 days), cross-Region included.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing between this and SnapMirror
&lt;/h2&gt;

&lt;p&gt;The shared-vault path and SnapMirror carry different things. This is not a ranking.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;SnapMirror&lt;/th&gt;
&lt;th&gt;Shared LAG vault&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What sits at the destination&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;DP&lt;/code&gt; volume, always present&lt;/td&gt;
&lt;td&gt;a recovery point, no volume until you restore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account boundary&lt;/td&gt;
&lt;td&gt;same account (cluster peering)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;owner and recipient can be different accounts&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPO&lt;/td&gt;
&lt;td&gt;down to 5 min&lt;/td&gt;
&lt;td&gt;the backup interval (guidance 60 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;To use it&lt;/td&gt;
&lt;td&gt;break and promote&lt;/td&gt;
&lt;td&gt;restore → a new volume (&lt;strong&gt;22.7 min, n=1, 1 GiB&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who starts the restore&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;the CLI; the portal visualizes only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failback path&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;snapmirror resync&lt;/code&gt; the other way&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stated symmetrically, including the recommended option's constraints: the shared-vault path takes 22.7 min to restore (n=1, 1 GiB) and its restore does not complete from the portal; SnapMirror needs cluster peering and a destination file system standing at all times. Running both is coherent — SnapMirror for availability, the shared LAG vault for a tamper-resistant retention layer in a separate account.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Each of these needs a real environment or a different org/account shape than this run had, so they are tracked as follow-up issues rather than folded in here. The plan is to cover them in later posts in this series.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure the cross-Region restore (E6)&lt;/strong&gt; into a recipient file system in that Region. This run kept to visibility and the same-path restore. Tracked in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/issues/479" rel="noopener noreferrer"&gt;issue #479&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the organization-sharing path (E13).&lt;/strong&gt; This run used the invitation path only. Tracked in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/issues/479" rel="noopener noreferrer"&gt;issue #479&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run MPA on a two-org, approval-team setup.&lt;/strong&gt; It is design-only here. Tracked in &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/issues/471" rel="noopener noreferrer"&gt;issue #471&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most useful thing I learned was smaller than the feature: being &lt;em&gt;shared&lt;/em&gt; and having the data &lt;em&gt;materialized on the recipient side&lt;/em&gt; are different states. Revoking the share removes visibility — an owner-side action — but a restored volume belongs to the recipient and stays.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

&lt;p&gt;Earlier post: &lt;a href="https://dev.to/aws-builders/aws-backup-now-copies-fsx-for-ontap-backups-across-regions-and-accounts-a-new-option-measured-3f60"&gt;AWS Backup now copies FSx for ONTAP backups across Regions and accounts — measured Tokyo to Osaka&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Japanese version (はてなブログ): &lt;a href="https://hakobiya.hatenablog.com/entry/FSx-for-ONTAP-shared-backup-vault-visibility" rel="noopener noreferrer"&gt;RAM で共有した論理エアギャップボールトを、別アカウント・別リージョンから見て復元する&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;The lab that produced these screenshots — the CloudFormation templates, the scripts, and the verification record — is here: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-S3AccessPoints-Serverless-Patterns/tree/main/infrastructure/shared-backup-vault-lab" rel="noopener noreferrer"&gt;infrastructure/shared-backup-vault-lab&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-backup-air-gapped-vault-fsx-ontap/" rel="noopener noreferrer"&gt;What's New: AWS Backup adds logically air-gapped vault support for Amazon FSx for NetApp ONTAP&lt;/a&gt; — the release this post verifies&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/aws-backup/latest/devguide/logicallyairgappedvault.html" rel="noopener noreferrer"&gt;AWS Backup: Logically air-gapped vault&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/aws-backup/latest/devguide/create-cross-account-backup.html" rel="noopener noreferrer"&gt;AWS Backup: Creating backup copies across AWS accounts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ram/latest/userguide/getting-started-sharing.html" rel="noopener noreferrer"&gt;AWS RAM: Sharing your resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/aws-backup/latest/devguide/API_ListBackupVaults.html" rel="noopener noreferrer"&gt;AWS Backup: ListBackupVaults / &lt;code&gt;--by-shared&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/aws-backup/latest/devguide/mpa-approval-team.html" rel="noopener noreferrer"&gt;AWS Backup: Multi-party approval (MPA)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/to-restore-backups.html" rel="noopener noreferrer"&gt;AWS: Restoring an FSx for ONTAP backup to a new volume&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/blogs/storage/inside-the-aws-storage-assessment-how-intelligent-analysis-replaces-guesswork/" rel="noopener noreferrer"&gt;AWS Storage Blog: Inside the AWS Storage Assessment — how intelligent analysis replaces guesswork&lt;/a&gt; — related for planning: its workload classification and data-protection cost modeling inform the standing-file-system vs. backup-only cost trade-off discussed above&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The file systems, SVMs, volumes, standard vault, and restored volume used for this test have been deleted and billing is stopped. The logically air-gapped vaults, under a compliance-mode lock, cannot be deleted until their recovery points' retention (source-backup creation time + 7 days) expires, and are deleted after that. The figures are measurements from one specific environment and configuration (n=1), and will differ with data volume and setup.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>storage</category>
      <category>security</category>
      <category>disasterrecovery</category>
    </item>
    <item>
      <title>I measured the speed and monthly cost of Amazon S3 Files, which lets you use S3 as files (and how it differs from FSx for ONTAP)</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:20:30 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-measured-the-speed-and-monthly-cost-of-amazon-s3-files-which-lets-you-use-s3-as-files-and-how-1970</link>
      <guid>https://dev.to/aws-builders/i-measured-the-speed-and-monthly-cost-of-amazon-s3-files-which-lets-you-use-s3-as-files-and-how-1970</guid>
      <description>&lt;h2&gt;
  
  
  Conclusion first: the one-client speed limit, scaling with client count, and a monthly cost order that splits by object size
&lt;/h2&gt;

&lt;p&gt;Amazon S3 Files lets you open, read, and write the data in S3 like an ordinary folder. With plain S3, a program has to call S3-specific APIs. With S3 Files, tools built to open files can work on your S3 data as they are.&lt;/p&gt;

&lt;p&gt;This post is a record of measurements and estimates for people who do not run storage performance tests as a routine job, to help decide whether S3 Files belongs in a design. I built the environment myself in ap-northeast-1 and measured speed on 2026-10-10. The monthly cost is not a measurement. It is arithmetic from list prices read on 2026-08-09. When I re-read the 2026-08-09 record with the Price List API on 2026-10-16, the values were the same (it covers 10 items and does not include the two PUT request prices).&lt;/p&gt;

&lt;p&gt;Three things came out of it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One server has a read speed limit. On a c5n.9xlarge (a 36 vCPU server) it was about 1.1 GB per second (&lt;strong&gt;1,104.9 MB/s&lt;/strong&gt;). It stops when the relay program running on the server (efs-proxy) uses up one CPU core (96.8 to 97.9%). One run.&lt;/li&gt;
&lt;li&gt;Adding servers raises the total. Going from 1 to 8 c5n.2xlarge servers raised the total from 784.4 MB/s to &lt;strong&gt;6,282.6 MB/s&lt;/strong&gt; (8.01x). All servers read the same 600 GiB file.&lt;/li&gt;
&lt;li&gt;Under this model's base assumptions, the monthly cost order against FSx for ONTAP splits by object size. Amazon FSx for NetApp ONTAP (FSx for ONTAP below) is an AWS managed service that provides NFS and SMB shared folders, and with S3 Access Points attached it also serves the same data through the S3 API. I fixed the stored volume at 20 TiB and estimated five patterns that differ in monthly read and write volume (P1, P4, P5, P6, P8; the contents are in the table in the chapter "Monthly cost estimate"). At 64 KiB, FSx for ONTAP was cheaper in all five ($1,783.11 to $2,218.13 a month, against $7,928.32 to $12,030.81 via S3 Files). At 4 MiB, S3 Files was cheaper in all five ($512.14 to $543.52 a month, against $1,781.28 to $1,788.08 on FSx for ONTAP).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The monthly amounts are subtotals of the main line items for ap-northeast-1, with prices read on 2026-08-09. A main-line-item subtotal is the sum of storage, S3 Files high-performance storage (a low-latency layer inside the file system; you pay storage charges for what it holds), fixed bandwidth, requests, and so on, and it leaves out any item that is not in the tables. They are price x quantity arithmetic, not measurements, and they are not invoices. The FSx for ONTAP capacity pool (the tier that holds data that does not fit on SSD) read request count is an assumed value that I have not confirmed. The P5 write volume and the read and write volumes of P1, P6, and P8 are placeholders with no source.&lt;/p&gt;

&lt;p&gt;Because the order was the same in all five patterns, what splits the order in this model is the object size, not the read and write volume. The candidate for the dividing line is the S3 Files import threshold (the file-size boundary for importing into high-performance storage; default 128 KiB). I estimated only two sizes, 64 KiB and 4 MiB, and I did not estimate the sizes at the boundary.&lt;/p&gt;

&lt;p&gt;The takeaways: when one server is not fast enough, add servers (confirmed up to 8). Under the base assumptions, FSx for ONTAP was cheaper at 64 KiB and S3 Files was cheaper at 4 MiB. The order can flip when the assumptions change, so replace them with your own values and check. The assumptions to replace are the share of data on the capacity pool, the storage efficiency of the SSD tier, the active share of S3 Files high-performance storage, the S3 Files import threshold, and the FSx for ONTAP deployment. The table of values at which the order flips is in the section "Baseline assumptions and sensitivity" of the generated document in the repository (the link is in the "Assumptions under which the order can flip" section of the chapter "Monthly cost estimate"). This post gives only the directions. The estimate itself is in the chapter "Monthly cost estimate".&lt;/p&gt;

&lt;h3&gt;
  
  
  How to read this post by goal
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You only want the conclusion: this chapter is enough.&lt;/li&gt;
&lt;li&gt;You want to know whether it works in your setup: "The value of S3 Files and four prerequisites"&lt;/li&gt;
&lt;li&gt;The mount does not proceed: "A hung mount"&lt;/li&gt;
&lt;li&gt;You want to know how it differs from FSx for ONTAP: "Comparing two ways to use the same data through two entry points"&lt;/li&gt;
&lt;li&gt;You want the cost: "Monthly cost estimate"&lt;/li&gt;
&lt;li&gt;You want to know which setup to consider first: "Choosing between S3 bucket first and file system first" at the end of the "Monthly cost estimate" chapter, and "Closing"&lt;/li&gt;
&lt;li&gt;You want the measurement conditions and limits: "The single-mount ceiling", "An aggregate that scales with client count", "An invalid warm / cold comparison", "Status of the four stated values", "What I have not measured"&lt;/li&gt;
&lt;li&gt;You want to reproduce it: "Reproducing it yourself"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There are times when you want to handle S3 data as files: an existing script that opens the files in a folder one by one, or a business tool built around a shared folder. Amazon S3 Files became generally available in April 2026, and it lets you mount an S3 bucket as an NFS file system (the network shared-folder mechanism of Linux) without migrating data.&lt;/p&gt;

&lt;p&gt;Before using it, I think you wonder about three things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is it good for, in the end?&lt;/li&gt;
&lt;li&gt;How fast is it, and what does it cost?&lt;/li&gt;
&lt;li&gt;How does it differ from a file-server type of service (FSx for ONTAP)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post answers the second and third. The speeds are values I measured in my own environment, and the monthly cost is a unit price x quantity estimate. For the differences from FSx for ONTAP, I put my past measurement records (the environment differs per measurement) and the official documentation side by side in a table. I do not cover the first, the introduction. The &lt;a href="https://dev.classmethod.jp/ja/articles/amazon-s3-files-ga-mount-and-compare-efs/" rel="noopener noreferrer"&gt;Classmethod article&lt;/a&gt; (Japanese) compares I/O performance and sync lag with Amazon EFS, and the &lt;a href="https://blog.serverworks.co.jp/s3-mount-services-comparison" rel="noopener noreferrer"&gt;Serverworks article&lt;/a&gt; (Japanese) sorts out five services, including Mountpoint for Amazon S3.&lt;/p&gt;

&lt;p&gt;In the previous post (&lt;a href="https://hakobiya.hatenablog.com/entry/fsxn-s3burst-throughput-ceilings-three-paths" rel="noopener noreferrer"&gt;S3 Burst Part 2: "The throughput ceilings of three paths"&lt;/a&gt;) I compared three paths, S3 Files among them, side by side and asked where each one tops out. This time I narrow down to S3 Files alone. Last time the question was which path tops out where. This time it is how S3 Files behaves as a file system.&lt;/p&gt;

&lt;p&gt;The mount failed before I could measure anything, so I recorded the cause first. Someone else can hit the same wall. After reading, you can decide, on numbers that come with their environments, whether S3 Files is a candidate, how far the number of servers can carry your speed, and which of S3 Files and FSx for ONTAP with S3 Access Points (the S3 API entry point that you attach to a volume) fits better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I cover / do not cover
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Covered
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The prerequisites of S3 Files and the value AWS states for it. Prerequisites decide first whether you can use it at all.&lt;/li&gt;
&lt;li&gt;Why the mount failed. It was missing permissions, not the network, and knowing in advance avoids it.&lt;/li&gt;
&lt;li&gt;The single-mount ceiling (c5n.9xlarge). The per-client ceiling is the starting point of capacity planning.&lt;/li&gt;
&lt;li&gt;Aggregate growth with client count (c5n.2xlarge, 1 to 8 clients). It shows how far clients can carry you past one mount's limit.&lt;/li&gt;
&lt;li&gt;The comparison with FSx for ONTAP S3 Access Points. Both give you the same data through files and the S3 API, but the source of truth, the speed of propagation, the growth with client count, and the billing items differ.&lt;/li&gt;
&lt;li&gt;The monthly cost estimate. With the stored volume fixed at 20 TiB, five read and write patterns, and two object sizes, I line up the main-line-item subtotals of S3 Files and FSx for ONTAP. It separates this model's result, in which the size splits the order, from the assumptions under which the order can flip.&lt;/li&gt;
&lt;li&gt;The accounting assumption for S3 Files metadata charges, and the reconciliation of usage against the bill. It separates what the reconciliation showed from the fact that the invoiced amount itself is unconfirmed.&lt;/li&gt;
&lt;li&gt;Reading data on the capacity pool. It separates the existence of billing items from how far a latency observation can be read.&lt;/li&gt;
&lt;li&gt;How my warm / cold comparison failed. Warm means reading data that sits in S3 Files' high-performance storage; cold means reading straight from the bucket. Someone repeating the measurement can fall into the same trap.&lt;/li&gt;
&lt;li&gt;Which of the four values AWS states I could confirm, and which I could not. It separates what is safe to base an adoption on.&lt;/li&gt;
&lt;li&gt;What I have not measured. It lets you judge where the numbers apply.&lt;/li&gt;
&lt;li&gt;How to reproduce it in your environment. It points to the runbooks so you can follow the measurements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Not covered
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;An introduction to S3 Files, and how it differs from Mountpoint for Amazon S3 and Amazon EFS. Articles already cover that, and it is outside this post's subject, measured speed and monthly cost.&lt;/li&gt;
&lt;li&gt;Which configuration is faster. The paths were measured in different environments (NFS version, instance type, configuration), so no same-conditions winner exists.&lt;/li&gt;
&lt;li&gt;Measurements that separate the propagation conditions. The window of about 60 seconds between writing a file and reading it through the S3 API appears as two rows in the comparison table of "Comparing two ways to use the same data through two entry points", and I explain it in four stages. Measurements that separate conditions, such as long continuous appends and concurrent updates, exist only as a plan and have no measured results. The conditions of that window are in &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L42-L46" rel="noopener noreferrer"&gt;another verification record&lt;/a&gt; (Japanese). It is outside this post's subject, which is throughput and client count, and that measurement covers a single condition.&lt;/li&gt;
&lt;li&gt;An explanation of the values at which the order flips. The table of values is in the generated document, and this post gives only the directions, because the values have to be read with the sourced ranges and the mechanical search ranges kept apart. I did not estimate the boundary object size (around 128 KiB).&lt;/li&gt;
&lt;li&gt;Invoiced amounts. The monthly cost is a unit price x quantity estimate, and I have not confirmed that it matches a bill.&lt;/li&gt;
&lt;li&gt;For the warm / cold difference, which path a first read takes (straight from the bucket, or from high-performance storage after import). I re-measured the latency with a different design, but &lt;code&gt;DataReadBytes&lt;/code&gt; was the same for the first and second reads, so I cannot distinguish the path. The values I measured are in the chapter "An invalid warm / cold comparison" (&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/s3files-warm-cold-latency-measured.md" rel="noopener noreferrer"&gt;verification record&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The tiering difference from a client outside AWS, and from a standby-AZ client. I measured Single-AZ and the preferred AZ of Multi-AZ first generation from an EC2 instance inside a VPC (&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/fsx-ontap-tier-latency-in-vpc-measured.md" rel="noopener noreferrer"&gt;verification record&lt;/a&gt;). The chapter "What I have not measured" lists the remaining scope in a table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Terms used in this post
&lt;/h2&gt;

&lt;p&gt;Each term is explained at first use in the text. This table is an index for rereading.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Where it appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Files&lt;/td&gt;
&lt;td&gt;A service that mounts an S3 bucket as an NFS file system. Built on Amazon EFS; the bucket remains the source of truth&lt;/td&gt;
&lt;td&gt;All chapters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-performance storage&lt;/td&gt;
&lt;td&gt;The low-latency layer inside the S3 Files file system. It holds the data and metadata of files in active use, and you pay storage charges for what it holds&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "Comparing two ways to use the same data through two entry points", "Monthly cost estimate", "An invalid warm / cold comparison"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;efs-proxy&lt;/td&gt;
&lt;td&gt;A local proxy that runs on the mounting client's EC2 instance. The mount target is 127.0.0.1, and in the verification records this one process handles TLS termination and NFS translation&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "A hung mount", "The single-mount ceiling", "An aggregate that scales with client count", "Comparing two ways to use the same data through two entry points"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mount target&lt;/td&gt;
&lt;td&gt;The NFS endpoint of the file system that you create in a VPC (port 2049). One per AZ at most&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "A hung mount"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nconnect&lt;/td&gt;
&lt;td&gt;A Linux NFS mount option that opens several TCP connections to one server. S3 Files does not support it, and specifying it hung the mount&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "The single-mount ceiling", "Comparing two ways to use the same data through two entry points"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm / cold&lt;/td&gt;
&lt;td&gt;In this post, used only for S3 Files reads. Warm is a read of data held in high-performance storage; cold is a read straight from the bucket. For FSx for ONTAP I write "on SSD" and "on the capacity pool"&lt;/td&gt;
&lt;td&gt;"An invalid warm / cold comparison", "Status of the four stated values", "What I have not measured"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity pool&lt;/td&gt;
&lt;td&gt;The tier where FSx for ONTAP keeps data that does not fit on SSD. Its storage price is lower than SSD (Single-AZ, ap-northeast-1: $0.0238 vs. $0.15 per GB-month, read 2026-08-09)&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points", "Monthly cost estimate", "What I have not measured"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiering All&lt;/td&gt;
&lt;td&gt;One of the tiering policies of an FSx for ONTAP volume. It places the data on the capacity pool&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points", "Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexCache / Origin&lt;/td&gt;
&lt;td&gt;FlexCache is an ONTAP feature in which a Cache volume holds only the data read from the Origin volume that owns the original data. This post uses it only to say that the FSx for ONTAP propagation measurement read the Origin volume directly&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Access Points&lt;/td&gt;
&lt;td&gt;In this post, the S3 API entry point attached to an FSx for ONTAP volume. The same data can be read and written over NFS / SMB and the S3 API&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points", "Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import threshold&lt;/td&gt;
&lt;td&gt;The size at which S3 Files decides whether to import a file into high-performance storage (&lt;code&gt;sizeLessThan&lt;/code&gt;). The default is 128 KiB, and sizes below it are imported. Exactly 128 KiB is treated as not imported&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "Monthly cost estimate", "An invalid warm / cold comparison", "Closing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern (P1, P4, P5, P6, P8)&lt;/td&gt;
&lt;td&gt;A name for a combination of monthly read and write volumes. The numbers follow the cost model's numbering, and the figures and tables carry five of them (P2, P3, and P7 are left out because they cost the same as P4 and P6)&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate", "Closing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working set / working-set ratio&lt;/td&gt;
&lt;td&gt;The working set is the part that is read within the month. The working-set ratio is its share of the stored volume (10% for P4)&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Churn rate&lt;/td&gt;
&lt;td&gt;The share of the stored volume that is rewritten in a month (5% for P5)&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main-line-item subtotal&lt;/td&gt;
&lt;td&gt;The sum of storage, S3 Files high-performance storage, fixed bandwidth, requests, and so on. Items not in the tables are not included. It is price x quantity arithmetic, not a measurement, and not an invoice&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate", "Closing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Placeholder&lt;/td&gt;
&lt;td&gt;A value with no source, put in to make the arithmetic work. It is not taken from a real workload&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VDBENCH&lt;/td&gt;
&lt;td&gt;A tool that generates storage I/O load (distributed by Oracle). The S3 Files measurements here used 5.04.07&lt;/td&gt;
&lt;td&gt;"The single-mount ceiling", "An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_direct&lt;/td&gt;
&lt;td&gt;A setting that reads and writes without the OS page cache. In VDBENCH it is &lt;code&gt;openflag=o_direct&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;"The single-mount ceiling", "An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;disjoint / same&lt;/td&gt;
&lt;td&gt;Disjoint: each client reads a non-overlapping range. Same: all clients read the same file&lt;/td&gt;
&lt;td&gt;"An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shared epoch&lt;/td&gt;
&lt;td&gt;All clients wait until the same UNIX time (90 seconds after launch) and start measuring together&lt;/td&gt;
&lt;td&gt;"An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The value of S3 Files and four prerequisites
&lt;/h2&gt;

&lt;p&gt;S3 Files lets Linux compute mount a bucket as an NFS file system and read and write it while the bucket stays the source of truth. It is built on Amazon EFS and became generally available in 34 regions in April 2026 (&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-s3-files/" rel="noopener noreferrer"&gt;What's New&lt;/a&gt;). Four prerequisites decide first whether you can use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four prerequisites
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prerequisite&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bucket versioning&lt;/td&gt;
&lt;td&gt;Required. Encryption is SSE-S3 or SSE-KMS&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html" rel="noopener noreferrer"&gt;Prerequisites for S3 Files&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client compute&lt;/td&gt;
&lt;td&gt;Linux on AWS (EC2 / Lambda / ECS / EKS / Fargate / Batch). I could not find documentation for mounting from outside AWS&lt;/td&gt;
&lt;td&gt;&lt;a href="https://aws.amazon.com/s3/features/files/" rel="noopener noreferrer"&gt;S3 Files product page&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;NFSv4.1 and NFSv4.2 only. NFSv3 and SMB are not listed as supported. nconnect (a Linux mount option that opens several TCP connections to one server) is unsupported; in my verification, specifying it timed out the mount after 90 seconds (one observation, c5n.2xlarge, 2026-09-02)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;Unsupported features, limits, and quotas&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/throughput-iops-concurrency.md#L1175-L1179" rel="noopener noreferrer"&gt;verification record&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File-to-bucket propagation&lt;/td&gt;
&lt;td&gt;After you change a file, S3 Files waits about 60 seconds before copying it to the bucket&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How the mount works
&lt;/h3&gt;

&lt;p&gt;When you mount, efs-proxy (the local proxy mentioned in the summary) starts on the client's EC2 instance. Your application mounts 127.0.0.1, and efs-proxy connects to the mount target in the VPC. The mount target is the NFS endpoint of the file system (port 2049), with a limit of one per AZ. In the verification records, this efs-proxy handles TLS termination and NFS translation in one process (&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L81-L84" rel="noopener noreferrer"&gt;verification record&lt;/a&gt;, Japanese). This structure sets the single-mount ceiling measured in a later chapter.&lt;/p&gt;

&lt;p&gt;Reads take one of two paths. Small files (under 128 KiB by default) are imported asynchronously into high-performance storage, the low-latency layer inside the file system, on first access, and later reads are fast. Reads of 1 MiB or more go straight to the bucket even when the data sits in high-performance storage. The source is &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Values AWS states
&lt;/h3&gt;

&lt;p&gt;The following four points are AWS's statements. I could get numbers for the second point, propagation time between files and the S3 API, and for the fourth point, single-mount throughput and aggregate growth with client count (up to 8). I did not measure the first, and the comparison for the third was invalid.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing file-based tools work as they are. The product page says "No code changes, no custom connectors, and no new APIs to learn" (&lt;a href="https://aws.amazon.com/s3/features/files/" rel="noopener noreferrer"&gt;product page&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The same data is usable from both files and the S3 API at the same time. The product page's wording is "nothing to reconcile between two separate systems", meaning you do not have to reconcile two systems.&lt;/li&gt;
&lt;li&gt;Only frequently read data sits in high-performance storage, and you pay for what it holds. Reads of 1 MiB or more cost only the standard S3 GET price (&lt;a href="https://aws.amazon.com/s3/features/files/" rel="noopener noreferrer"&gt;product page&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Many clients can use it at once. The limit is 25,000 connections per file system, and the aggregate read throughput is "up to terabytes per second" in the AWS pages' wording (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;quotas&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your requirements include NFSv3 or fixed SMB devices or Windows workflows, S3 Files drops out at the prerequisites table. The AWS documentation does not mention mounting from outside AWS that I could find. If you need it, check in your own environment before you put S3 Files on the candidate list. If none of those apply, read the following chapters on the mount failure, the single-mount ceiling, and client scaling, and use them for capacity planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hung mount: missing CloudWatch permissions on the client's IAM role
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mount -t s3files&lt;/code&gt; timed out every 15 seconds. The cause was not the network or DNS. &lt;strong&gt;The IAM role on the client EC2 instance lacked &lt;code&gt;logs:CreateLogGroup&lt;/code&gt; and &lt;code&gt;cloudwatch:PutMetricData&lt;/code&gt;.&lt;/strong&gt; Adding the permissions let the mount complete.&lt;/p&gt;

&lt;p&gt;The environment was a c5n.9xlarge (Amazon Linux 2023, &lt;code&gt;amazon-efs-utils&lt;/code&gt; 3.3.2) in a dedicated measurement VPC in ap-northeast-1 (a public subnet with an internet gateway). It happened while I prepared the measurement, and my troubleshooting notes carry no date. The throughput measurements in the same environment ran on 2026-10-10.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: timeouts every 15 seconds with a healthy network
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;efs-proxy: running, with a TCP connection to port 2049 on the mount target (ESTAB in &lt;code&gt;ss&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Reachability: both S3 on 443 and the service endpoint on 2049 were reachable&lt;/li&gt;
&lt;li&gt;Mount: &lt;code&gt;mount.nfs4 127.0.0.1:/&lt;/code&gt; still did not finish&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cause: efs-proxy restarting over and over because log creation failed
&lt;/h3&gt;

&lt;p&gt;The efs-proxy log contained 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;WARN  cw_publisher Failed to create log group: AccessDeniedException ... logs:CreateLogGroup
ERROR nfs::nfs_reader Error sending message batch to dispatcher DispatchingFailure
INFO  controller Proxy incarnation restarted, restart_count=3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before establishing the NFS connection, the mount helper tries to create a CloudWatch Logs log group (&lt;code&gt;/aws/efs/utils&lt;/code&gt;). When that call fails for lack of permissions, efs-proxy keeps restarting and the mount never finishes. An AWS article names the same behavior as the root cause when a mount hangs forever without an obvious reason (&lt;a href="https://repost.aws/articles/ARPg3C_iUzSkWA3l1gdUPUJA/mounting-amazon-s3-files-on-ec2-instances-in-private-subnets-without-internet-access" rel="noopener noreferrer"&gt;Mounting S3 Files on EC2 in private subnets&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;My subnet has an internet gateway, so CloudWatch Logs was reachable. Permissions were the blocker, not reachability. In a private subnet with no CloudWatch Logs VPC endpoint, unreachability produces the same symptom.&lt;/p&gt;

&lt;p&gt;Original record: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-mount-pitfalls.md#L25-L61" rel="noopener noreferrer"&gt;Mount pitfalls (causes and fixes)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixes: any of three options works
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Fits when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Grant the IAM role &lt;code&gt;logs:CreateLogGroup&lt;/code&gt;, &lt;code&gt;logs:CreateLogStream&lt;/code&gt;, &lt;code&gt;logs:PutLogEvents&lt;/code&gt;, and &lt;code&gt;cloudwatch:PutMetricData&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;You have an internet gateway or CloudWatch endpoints and also want CloudWatch metrics. I used this one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Disable CloudWatch logging in the mount helper (&lt;code&gt;/etc/amazon/efs/s3files-utils.conf&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;You do not want to add permissions or endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Create a CloudWatch Logs VPC endpoint (&lt;code&gt;com.amazonaws.&amp;lt;region&amp;gt;.logs&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A private subnet with neither an internet gateway nor a NAT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I did not pick option 2 because the next chapter's measurement uses efs-proxy's CloudWatch metrics (connectivity and byte counts) to isolate the cause of the ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-mount checks: three items
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;IAM role: it carries the permissions from fix 1.&lt;/li&gt;
&lt;li&gt;botocore: installed with &lt;code&gt;dnf install python3-botocore&lt;/code&gt; (&lt;code&gt;pip3&lt;/code&gt; does not work). Without it, the mount either stopped with &lt;code&gt;access denied by server while mounting&lt;/code&gt;, or went through but CloudWatch metrics were unavailable. The two verification records show different symptoms, and neither records the condition that decides which one you get.&lt;/li&gt;
&lt;li&gt;Mount target: it is in the same AZ as the client, and the security group allows 2049 from the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check these three items before you mount, and decide beforehand whether to grant the CloudWatch permissions or disable logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single-mount ceiling: one efs-proxy core saturated
&lt;/h2&gt;

&lt;p&gt;A single mount stopped at &lt;strong&gt;1,104.9 MB/s&lt;/strong&gt; for 1 MiB sequential reads (c5n.9xlarge, one run). The limiter was the efs-proxy process's CPU: one core of 36 vCPU was full (96.8 to 97.9%). As noted in the prerequisites chapter, S3 Files does not support nconnect, so no mount option can add connections to efs-proxy and move this ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement conditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Environment: ap-northeast-1, 2026-10-10 (UTC), single AZ, c5n.9xlarge (36 vCPU), Amazon Linux 2023&lt;/li&gt;
&lt;li&gt;Mount: &lt;code&gt;nfs4 vers=4.2&lt;/code&gt;, &lt;code&gt;rsize=wsize=1048576&lt;/code&gt;, &lt;code&gt;hard&lt;/code&gt;. The server is &lt;code&gt;127.0.0.1&lt;/code&gt; (through efs-proxy, TLS and IAM authentication always on)&lt;/li&gt;
&lt;li&gt;Load: VDBENCH 5.04.07 (an I/O load generator distributed by Oracle), 512 threads, &lt;code&gt;o_direct&lt;/code&gt; (bypasses the OS page cache)&lt;/li&gt;
&lt;li&gt;Data: a single uncompressible 600 GiB file. 60 seconds of warmup, then 300 seconds measured&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results: four workloads
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;IOPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB sequential read&lt;/td&gt;
&lt;td&gt;1,104.9 MB/s&lt;/td&gt;
&lt;td&gt;1,105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB sequential write&lt;/td&gt;
&lt;td&gt;852.0 MB/s&lt;/td&gt;
&lt;td&gt;852&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random read&lt;/td&gt;
&lt;td&gt;61.9 MB/s&lt;/td&gt;
&lt;td&gt;15,839&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random write&lt;/td&gt;
&lt;td&gt;14.4 MB/s&lt;/td&gt;
&lt;td&gt;3,688&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each row is one run; I did not repeat any condition. Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L53-L61" rel="noopener noreferrer"&gt;Results (S-1 + S-2)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolating the cause: efs-proxy CPU saturated at one core
&lt;/h3&gt;

&lt;p&gt;I re-measured 1 MiB sequential reads for 60 seconds and sampled CPU three times.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;efs-proxy CPU&lt;/th&gt;
&lt;th&gt;VDBENCH and java CPU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;96.8%&lt;/td&gt;
&lt;td&gt;near 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;97.4%&lt;/td&gt;
&lt;td&gt;near 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;97.9%&lt;/td&gt;
&lt;td&gt;near 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The re-measured throughput was 1,109.4 MB/s, 0.4% away from the main run's 1,104.9 MB/s. The mount target is a local proxy at &lt;code&gt;127.0.0.1&lt;/code&gt;, and efs-proxy handles TLS termination and NFS translation in one process, so one mount tops out at one core of that process. Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L72-L76" rel="noopener noreferrer"&gt;Ceiling confirmation&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Differences by instance type: three values under different conditions
&lt;/h3&gt;

&lt;p&gt;Even on the same c5n.2xlarge the value changes with how I measured, so here are three side by side.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Instance&lt;/th&gt;
&lt;th&gt;Conditions&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stream-count sweep&lt;/td&gt;
&lt;td&gt;c5n.2xlarge&lt;/td&gt;
&lt;td&gt;8 streams, 1 GiB per stream, &lt;code&gt;O_DIRECT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;451.3 MB/s&lt;/td&gt;
&lt;td&gt;2026-09-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One client in the client-count test&lt;/td&gt;
&lt;td&gt;c5n.2xlarge&lt;/td&gt;
&lt;td&gt;VDBENCH, 64 threads, 1 MiB sequential, 600 GiB file&lt;/td&gt;
&lt;td&gt;784.4 MB/s&lt;/td&gt;
&lt;td&gt;2026-10-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-mount test&lt;/td&gt;
&lt;td&gt;c5n.9xlarge&lt;/td&gt;
&lt;td&gt;VDBENCH, 512 threads, 1 MiB sequential, 600 GiB file&lt;/td&gt;
&lt;td&gt;1,104.9 MB/s&lt;/td&gt;
&lt;td&gt;2026-10-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three differ in conditions, so this is not a comparison that changes only the instance type. I have no measurement that varies only the instance type under the same conditions. I also did not isolate whether efs-proxy CPU or the client's network bandwidth sets the roughly 785 MB/s per c5n.2xlarge. Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/throughput-iops-concurrency.md#L1139-L1150" rel="noopener noreferrer"&gt;Stream-count measurement&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;When one mount is not enough bandwidth, design for adding clients (the next chapter).&lt;/p&gt;

&lt;h2&gt;
  
  
  An aggregate that scales with client count: 8.01x at 8 clients
&lt;/h2&gt;

&lt;p&gt;Going from 1 to 8 c5n.2xlarge clients raised the aggregate read from 784.4 MB/s to &lt;strong&gt;6,282.6 MB/s (8.01x)&lt;/strong&gt;. Per client it stayed roughly constant at 784.4 to 802.2 MB/s. That fits the structure in which each client has its own efs-proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement conditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Environment: ap-northeast-1, single AZ, 2026-10-10 (UTC), up to 8 c5n.2xlarge, Amazon Linux 2023&lt;/li&gt;
&lt;li&gt;Mount: each client mounts S3 Files on its own (one efs-proxy per client)&lt;/li&gt;
&lt;li&gt;Load: VDBENCH, 1 MiB sequential read, &lt;code&gt;o_direct&lt;/code&gt;, fixed at 64 threads per client (only the client count changes)&lt;/li&gt;
&lt;li&gt;Simultaneous start: shared epoch. All clients wait until the same UNIX time (90 seconds after launch) and start measuring together. 60 seconds of warmup, then 180 seconds measured&lt;/li&gt;
&lt;li&gt;Range read: two patterns, same (all clients read the same 600 GiB file) and disjoint (each client reads a non-overlapping range)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results: all clients read the same file
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Clients&lt;/th&gt;
&lt;th&gt;Aggregate&lt;/th&gt;
&lt;th&gt;Per client&lt;/th&gt;
&lt;th&gt;Ratio to 1 client&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;784.4 MB/s&lt;/td&gt;
&lt;td&gt;784.4 MB/s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1,604.5 MB/s&lt;/td&gt;
&lt;td&gt;802.2 MB/s&lt;/td&gt;
&lt;td&gt;2.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3,148.4 MB/s&lt;/td&gt;
&lt;td&gt;787.1 MB/s&lt;/td&gt;
&lt;td&gt;4.01x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6,282.6 MB/s&lt;/td&gt;
&lt;td&gt;785.3 MB/s&lt;/td&gt;
&lt;td&gt;8.01x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L134-L139" rel="noopener noreferrer"&gt;Client-count measurement (same)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Results: each client reads a non-overlapping range
&lt;/h3&gt;

&lt;p&gt;I measured disjoint at 8 clients only. The aggregate was 7,210.0 MB/s, and per client 901.3 MB/s (a spread of 770 to 1,332), higher than the 8-client same result of 6,282.6 MB/s. Each client reads different data, so sharing and fanning out one read cannot explain the number. I did not measure disjoint at 1, 2, or 4 clients, so the shape of disjoint scaling is unconfirmed.&lt;/p&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L143-L147" rel="noopener noreferrer"&gt;Client-count measurement (disjoint)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement limit: client-side totals only
&lt;/h3&gt;

&lt;p&gt;The numbers are the sum of what the clients reported. I could not reconcile them with CloudWatch &lt;code&gt;DataReadBytes&lt;/code&gt;: on the day, metrics arrived sparsely and did not land in the measurement window. The record also has no spread across repeated runs for each client count.&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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fab4d651fe338e8e885f468b6c93d2116a6f6cc2a%2Fdocs%2F_assets%2Fimages%2Fs3burst-s3files-internals-en%402x.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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fab4d651fe338e8e885f468b6c93d2116a6f6cc2a%2Fdocs%2F_assets%2Fimages%2Fs3burst-s3files-internals-en%402x.png" alt="The internals of Amazon S3 Files. Each client has its own efs-proxy that connects to the mount target over TLS, and S3 Files uses two paths,&lt;br&gt;
high-performance storage and direct bucket reads. A single mount tops out at one efs-proxy core, and the aggregate grows in proportion to client count up to 8 clients" width="800" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1: The internals of S3 Files. Each client has its own efs-proxy. A single mount tops out at one efs-proxy core, while the aggregate grows in proportion to client count (measured up to 8). The figure says the same thing as the tables above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For capacity planning up to 8 clients, multiply roughly 785 MB/s per client (785.3 MB/s at 8 clients; c5n.2xlarge, all clients reading the same file) by the client count. I have not measured 9 or more. If you have a use case that shares it across 9 or more clients, tell me the conditions on the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/issues/new?template=measurement-request.yml" rel="noopener noreferrer"&gt;repository issue form&lt;/a&gt;, and I will measure from the conditions people use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing two ways to use the same data through two entry points: where the source of truth sits
&lt;/h2&gt;

&lt;p&gt;Both S3 Files and FSx for ONTAP with S3 Access Points let you use the same data through a file mount and the S3 API. What differs is the source of truth: an S3 bucket for S3 Files, an ONTAP volume for FSx for ONTAP. That difference bears on the speed of propagation, how throughput grows as you add clients, and how the bill comes out. The measured values do not share an environment, though, so this post does not isolate whether the differences come from where the source of truth sits.&lt;/p&gt;

&lt;p&gt;The two setups compared in this chapter are as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 Files: attach S3 Files to an S3 bucket and mount it over NFS from EC2. The S3 API reaches the bucket directly.&lt;/li&gt;
&lt;li&gt;FSx for ONTAP: use an ONTAP volume over NFS / SMB and attach S3 Access Points (the S3 API entry point attached to the volume) to the same volume. Data that does not fit on SSD can sit on the capacity pool, a tier with a lower storage price. Tiering All, one of the volume's tiering policies, places the data on the capacity pool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The table below lines up the two setups by aspect. I wrote "not measured" where I did not measure. Measured values come from different environments, so each cell carries its environment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;S3 Files (S3 bucket first)&lt;/th&gt;
&lt;th&gt;FSx for ONTAP + S3 Access Points (file system first)&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;The S3 bucket. Objects and files point at the same bucket&lt;/td&gt;
&lt;td&gt;The ONTAP volume&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File-side entry&lt;/td&gt;
&lt;td&gt;NFSv4.1 / 4.2 only, through a per-client efs-proxy. nconnect is unsupported&lt;/td&gt;
&lt;td&gt;ONTAP serves NFS (v3, v4.0 to 4.2) and SMB directly&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L1165-L1169" rel="noopener noreferrer"&gt;Protocol comparison&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;S3 Files quotas&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 API-side entry&lt;/td&gt;
&lt;td&gt;The bucket itself&lt;/td&gt;
&lt;td&gt;An S3 Access Point attached to the volume&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Propagation: write as a file, read with the S3 API&lt;/td&gt;
&lt;td&gt;p50 63,769.3 ms (a window of about 60 seconds). t3.small, NFSv4.2, 64 B, concurrency 1, n=30, 2026-09-01&lt;/td&gt;
&lt;td&gt;p50 44 ms. NFSv3, 128 MBps Single-AZ, 64 B, concurrency 1, n=30, 2026-08-09&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L42-L45" rel="noopener noreferrer"&gt;S3 Files measurement&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L96-L101" rel="noopener noreferrer"&gt;FSx for ONTAP measurement&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Propagation: write with the S3 API, read as a file&lt;/td&gt;
&lt;td&gt;p50 1,533.1 ms (same environment as above)&lt;/td&gt;
&lt;td&gt;p50 3 ms. A direct NFS read of the Origin volume that received the write (it does not go through a FlexCache Cache volume; same environment as above)&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict and locking&lt;/td&gt;
&lt;td&gt;When the same key is changed from the file side and the S3 API side, the S3 bucket side became the source of truth (3/3 converged on the S3 content). The file-side version was moved to &lt;code&gt;.s3files-lost+found-&amp;lt;fs-id&amp;gt;&lt;/code&gt;. Only conflicts inside the window (5 seconds later) were measured&lt;/td&gt;
&lt;td&gt;Not measured. I did not measure what happens when the same key is changed from the file side and the S3 API side, nor reconcile it with ONTAP-side file locking&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L146-L160" rel="noopener noreferrer"&gt;Conflict behavior&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supported APIs, conditional writes, copy scope&lt;/td&gt;
&lt;td&gt;The S3 bucket API. I did not measure conditional writes or copy scope in this post&lt;/td&gt;
&lt;td&gt;Presigned URLs through S3 Access Points (&lt;code&gt;PutObject&lt;/code&gt; / &lt;code&gt;HeadObject&lt;/code&gt; / &lt;code&gt;GetObject&lt;/code&gt;) are verified (2026-08-19, n=30x4, succeeded under both SigV4 and SigV2). The official support table also lists &lt;code&gt;Presign&lt;/code&gt; as Supported (confirmed 2026-10-08, documented). Uploads are single &lt;code&gt;PutObject&lt;/code&gt; 5 GiB and whole object 50 GiB, with the limit judged at &lt;code&gt;CompleteMultipartUpload&lt;/code&gt;. The read side is asymmetric: a GET of 50 GiB + 1 byte succeeded (537 seconds, 2026-09-11)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification-status.md" rel="noopener noreferrer"&gt;Verification status&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Versioning / Object Lock / Lifecycle&lt;/td&gt;
&lt;td&gt;Bucket versioning is required. I did not measure Object Lock or Lifecycle in this post&lt;/td&gt;
&lt;td&gt;On the S3 Access Points side, S3 event notifications, lifecycle, and versioning are not supported (documented). ONTAP-side data protection (SnapLock and the like) is a separate mechanism; do not equate it with S3 Object Lock&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM vs. file-side permissions&lt;/td&gt;
&lt;td&gt;An object written through the S3 API appears as &lt;code&gt;root:root&lt;/code&gt; on the file system (file 0644, directory 0755). A client mapped to non-root at the access point cannot write into a directory the S3 API created (reads go through). The official &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-synchronization.html" rel="noopener noreferrer"&gt;Synchronization&lt;/a&gt; page says POSIX permissions are stored in the S3 object's user-defined metadata, and that objects without metadata get default permissions. I have not found the specific values &lt;code&gt;root:root&lt;/code&gt; or 0644 / 0755 in the pages I read&lt;/td&gt;
&lt;td&gt;ONTAP NFS / SMB file permissions and the IAM on the S3 Access Points side are separate paths. I did not run a reconciling measurement in this post&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L130-L134" rel="noopener noreferrer"&gt;Owner of S3-API writes&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where unread data sits&lt;/td&gt;
&lt;td&gt;The S3 bucket. Reads of 1 MiB or more go to the bucket even when the data is in high-performance storage&lt;/td&gt;
&lt;td&gt;SSD. Depending on the tiering policy, the capacity pool (Tiering All places everything there)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L1050-L1055" rel="noopener noreferrer"&gt;Billing items table&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read billing items&lt;/td&gt;
&lt;td&gt;S3 GET plus metadata reads ($0.04 / GB; the model counts 4 KiB x operations). Small files held in high-performance storage also incur a data read charge ($0.04 / GB). In two measurements, on 2026-10-16 and 2026-10-17, the Cost Explorer usage for &lt;code&gt;APN1-Files-Read&lt;/code&gt; matched the sum of CloudWatch &lt;code&gt;DataReadBytes&lt;/code&gt; and &lt;code&gt;MetadataReadBytes&lt;/code&gt; to within 0.5% (daily, &lt;code&gt;Estimated: true&lt;/code&gt;). Because cost is set by usage (bytes), the per-read operation breakdown does not change the subtotal. The invoiced amount is offset to a net zero by credits, so I have not confirmed it as a billed figure&lt;/td&gt;
&lt;td&gt;GET through S3 Access Points ($0.000029 / 1,000). Data on the capacity pool can incur a read request charge ($0.00037 / 1,000). The billable requests per GetObject are not confirmed. In a separate environment (2026-10-20, Single-AZ), counting CloudWatch &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; gave about 36 per object at 4 MiB, but that is a CloudWatch operation count, not a billable request count, and the correspondence between the two is unconfirmed&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L1050-L1055" rel="noopener noreferrer"&gt;Billing items table&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/fsx-ontap-cpool-read-operations-measured.md" rel="noopener noreferrer"&gt;capacity pool operation-count measurement&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency of reading data that is not held in the fast tier&lt;/td&gt;
&lt;td&gt;Not confirmed. 64 KiB p50 was 5.33 ms from high-performance storage and 4.05 ms straight from the bucket (c5n.9xlarge, n=30, one run, 2026-10-10). A separate t3.small record shows 10.8 ms vs. 55.0 ms (2026-09-01), which did not reproduce on c5n.9xlarge&lt;/td&gt;
&lt;td&gt;Measured from EC2 inside a VPC. For 4 MiB reads that reached the capacity pool, ALL was slower than SSD by a median difference of +122.4 ms (Single-AZ, 1 batch, n=30) and +110.4 ms (preferred AZ of Multi-AZ first generation, 4 batches, 120 pairs) (2026-10-20, ONTAP 9.19.1P3, 128 MBps, concurrency 1). A client outside AWS and a standby AZ are not measured&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L175-L179" rel="noopener noreferrer"&gt;Three reads&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L243-L247" rel="noopener noreferrer"&gt;Reads by tier&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/fsx-ontap-tier-latency-in-vpc-measured.md" rel="noopener noreferrer"&gt;Tier latency inside a VPC&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding clients: file path&lt;/td&gt;
&lt;td&gt;1 to 8 clients: 784.4 to 6,282.6 MB/s (8.01x). c5n.2xlarge, all clients read the same file, 2026-10-10. 9 or more: not measured&lt;/td&gt;
&lt;td&gt;1 to 8 clients: 2,664.58 to 11,916.29 MB/s (4.47x, all clients read the same file). With non-overlapping regions it does not grow, from 2,299.05 MB/s at 2 clients to 2,173.37 MB/s at 8. c5n.2xlarge, nconnect=16, second generation, 2,048 MBps, 2026-09-05&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L134-L139" rel="noopener noreferrer"&gt;S3 Files client count&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/perf-matrix-results.md#L421-L443" rel="noopener noreferrer"&gt;FSx for ONTAP client count&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding clients: S3 API path&lt;/td&gt;
&lt;td&gt;The S3 API reaches the bucket directly. Reads from Amazon S3 went from 495.4 to 2,030.5 MB/s at 1 to 4 clients (4.10x, c5n.2xlarge, 2026-09-01 to 02). Not measured on a bucket with S3 Files attached&lt;/td&gt;
&lt;td&gt;At 1 to 4 clients, writes went 129.4 to 131.0 MB/s (1.01x) and reads 480.9 to 612.3 MB/s (1.27x). 128 MBps configuration, nearly flat from 2 clients. Clients share the purchased throughput capacity&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/throughput-iops-concurrency.md#L784-L789" rel="noopener noreferrer"&gt;Measurement record of the previous post&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Published limits&lt;/td&gt;
&lt;td&gt;25,000 connections per file system, 3 GiB/s read per client. Aggregate read is "up to terabytes per second" in AWS's wording&lt;/td&gt;
&lt;td&gt;You buy throughput capacity. The minimum for Single-AZ first generation is 128 MBps&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;quotas&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L374-L376" rel="noopener noreferrer"&gt;Fixed-cost floor&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed cost&lt;/td&gt;
&lt;td&gt;None. S3 storage plus what you use&lt;/td&gt;
&lt;td&gt;Floor of $269.57 a month for Single-AZ first generation (SSD 1,024 GiB + 128 MBps, prices read 2026-08-09)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L374-L376" rel="noopener noreferrer"&gt;Fixed-cost floor&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prerequisites and constraints&lt;/td&gt;
&lt;td&gt;Bucket versioning required. Clients are Linux on AWS. NFSv4.1 / 4.2 only. About 60 seconds from file to bucket&lt;/td&gt;
&lt;td&gt;On the S3 Access Points side, S3 event notifications, lifecycle, and versioning are not supported. Capacity pool read charges. Clients share the throughput capacity&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The propagation rows are not a same-conditions comparison, because the NFS version, client, and configuration differ. The client-count rows differ too: S3 Files used VDBENCH with 64 threads per client, and FSx for ONTAP used a second-generation 2,048 MBps configuration with nconnect=16. I read in full on 2026-10-16 the two AWS documents this chapter relies on: the S3 User Guide pages &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-synchronization.html" rel="noopener noreferrer"&gt;Understanding how synchronization works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences table&lt;/a&gt;. For the measured rows, the sources are the links in the "Source" column.&lt;/p&gt;

&lt;h3&gt;
  
  
  The four stages of file-to-bucket propagation, and how to treat the maximum reflection time
&lt;/h3&gt;

&lt;p&gt;The "window of about 60 seconds" in the propagation rows is not one wait but four stages. The measured p50 of 63,769.3 ms is the whole span from (a) to (d).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(a) File write complete and persisted&lt;/td&gt;
&lt;td&gt;The moment the write to NFS finishes through efs-proxy. Separate from propagation to S3&lt;/td&gt;
&lt;td&gt;NFS semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(b) No-write wait&lt;/td&gt;
&lt;td&gt;The wait counted from when writes to the same file stop. The official Synchronization page states the "60 seconds with no writes" condition&lt;/td&gt;
&lt;td&gt;Documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(c) Export start&lt;/td&gt;
&lt;td&gt;The moment a file that satisfies the wait condition becomes an export target. The official Performance page states the roughly 60-second aggregation&lt;/td&gt;
&lt;td&gt;Documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(d) Visible through the S3 API&lt;/td&gt;
&lt;td&gt;After the export completes. The measured p50 of 63,769.3 ms is this whole (a) to (d)&lt;/td&gt;
&lt;td&gt;Measured (t3.small, 64 B, concurrency 1, n=30, 2026-09-01)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The export side agrees across two sources. Against the client-side max of 66.33 seconds, the CloudWatch &lt;code&gt;ExportAge&lt;/code&gt; peak was 66.06 seconds, a 0.27-second difference (all 30 runs fell in 63.8 to 66.3 seconds).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The maximum reflection time cannot be used as a guaranteed value.&lt;/strong&gt; On the import side (S3 to file), the client-side max of 14.36 seconds and the CloudWatch &lt;code&gt;ImportAge&lt;/code&gt; peak of 62.80 seconds do not agree. I could not find a maximum reflection time in the official documentation, so I do not treat it as an upper bound, and I write the representative value as the client-side p50. I do not write "reflected within 60 seconds", "reflected every minute even under continuous updates", or "never synchronized" either, because none has grounds. This explanatory model (wait, then aggregate, then export) is not an SLA.&lt;/p&gt;

&lt;p&gt;Three notes follow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a conflict (the same key changed from the file side and the S3 API side), the S3 bucket side became the source of truth (3/3 converged on the S3 content). The file-side version was moved to &lt;code&gt;.s3files-lost+found-&amp;lt;fs-id&amp;gt;&lt;/code&gt; (matching the CloudWatch &lt;code&gt;LostAndFoundFiles&lt;/code&gt; peak of 3.00).&lt;/li&gt;
&lt;li&gt;That directory is not created on conflict; it is present from the start (&lt;code&gt;0700 root:root&lt;/code&gt;), and what happens on conflict is the eviction event. The official Synchronization page says files in this directory are not copied to the S3 bucket (I did not measure this).&lt;/li&gt;
&lt;li&gt;For large rename / move operations, a rename on the file side finishes immediately, while on the S3 side it becomes a per-object copy and delete. AWS says a rename of 100,000 files takes several minutes to reflect on the S3 side, and that the prefixes from before and after the rename are both visible midway. I have not measured this behavior separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measurements that separate the propagation conditions (long continuous appends, after updates stop, while another file is being updated, concurrent updates) are deferred to &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/ab4d651fe338e8e885f468b6c93d2116a6f6cc2a/docs/ja/verification/s3files-sync-additional-plan.md" rel="noopener noreferrer"&gt;a separate verification plan&lt;/a&gt; (a plan, not a new measurement).&lt;/p&gt;

&lt;p&gt;What this comparison can settle is two things: whether the source of truth lives in an S3 bucket or an ONTAP volume, and whether you can accept the roughly 60 seconds from file to bucket. I estimate the monthly cost in the next chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monthly cost estimate: an order that splits by object size, and room for assumptions to move it
&lt;/h2&gt;

&lt;p&gt;I apply the same list prices to the two setups from the previous chapter, with the stored volume fixed at 20 TiB. I estimated 10 cells: 5 patterns that differ in monthly read and write volume (P1, P4, P5, P6, P8) times 2 object sizes (64 KiB and 4 MiB). The amounts in this chapter are list price x quantity for ap-northeast-1, with prices read on 2026-08-09, and they are subtotals of the main line items. The subtotals add up the line items shown in the tables, and some items are not included. They are not measurements and not invoices. "Base assumptions" below means the values in the common-assumptions table in the next section, "Five patterns".&lt;/p&gt;

&lt;p&gt;Under this model's base assumptions, in all 10 cells, &lt;strong&gt;the subtotal is lower on FSx for ONTAP at 64 KiB and lower via S3 Files at 4 MiB&lt;/strong&gt;. At 64 KiB, FSx for ONTAP comes to $1,783.11 to $2,218.13 a month and S3 Files to $7,928.32 to $12,030.81. At 4 MiB, S3 Files comes to $512.14 to $543.52 a month and FSx for ONTAP to $1,781.28 to $1,788.08. Because the order was the same in all five patterns, what splits the order in this model is the object size, not the read and write volume. The candidate for the dividing line is the S3 Files import threshold (default 128 KiB). I estimated two sizes, 64 KiB and 4 MiB, so the tables have no rows near 128 KiB or at 1 MiB. The reading that the dividing line sits at the threshold comes from those two points and from how the price works (high-performance storage is charged only for sizes below the threshold), and I did not estimate sizes at the boundary. The import condition is &lt;code&gt;sizeLessThan&lt;/code&gt;, so I treat exactly 128 KiB as not imported. This order is a result under the base assumptions, and it can flip when they change, so check the section "Assumptions under which the order can flip" in this chapter against your own values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five patterns: total monthly reads and writes
&lt;/h3&gt;

&lt;p&gt;Writing "read N times a month" alone does not fix the total volume read. Whether the N reads cover all objects or only some of them changes the total several times over. I define each pattern with three numbers: the working-set ratio, the number of reads per working-set object, and the write volume. The table shows the monthly read and write volumes derived from them. The working set is the part that is read within the month. The working-set ratio is the working set's share of the stored volume, and the number of reads per working-set object is how many times each of its objects is read in a month. I set the write volume as a share of the stored volume (the churn rate).&lt;/p&gt;

&lt;p&gt;Take P4 as an example. 10% of the 20 TiB stored (2 TiB) is the working set, and each of its objects is read 10 times a month. The volume read is 2 TiB x 10 = 20 TiB, which is 1.0x the stored volume. The other 90% (18 TiB) is not read at all within the month. With 4 MiB objects, that is 524,288 objects read 10 times each, or 5,242,880 reads. P4 has no writes, so the stored volume stays at 20 TiB.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Working-set ratio&lt;/th&gt;
&lt;th&gt;Reads per working-set object&lt;/th&gt;
&lt;th&gt;Monthly read volume (multiple of stored volume)&lt;/th&gt;
&lt;th&gt;Monthly write volume (multiple of stored volume)&lt;/th&gt;
&lt;th&gt;Basis of the values&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1 Cold archive&lt;/td&gt;
&lt;td&gt;5% (1 TiB)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 TiB (0.05x)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Placeholder (no source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 Re-read a small working set heavily&lt;/td&gt;
&lt;td&gt;10% (2 TiB)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;This model's estimate assumption (not a measurement)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 Read-mostly with light ingest&lt;/td&gt;
&lt;td&gt;10% (2 TiB)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;1 TiB (0.05x, churn rate 5%)&lt;/td&gt;
&lt;td&gt;Reads as in P4. The write volume is a placeholder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 Mixed read and write&lt;/td&gt;
&lt;td&gt;50% (10 TiB)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;40 TiB (2.0x)&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;Placeholder (no source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 Write-centred&lt;/td&gt;
&lt;td&gt;10% (2 TiB)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2 TiB (0.1x)&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;Placeholder (no source)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#the-patterns" rel="noopener noreferrer"&gt;The patterns&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The placeholder values are numbers I put in to make the arithmetic work. They are not taken from a real workload. Replace them with your own monthly read and write volumes. The cost model also has P2, P3, and P7, but I left them out of the figures and tables because they have the same monthly read and write volumes, and so the same amounts, as P4 and P6 (P4 matches P2 and P3, and P6 matches P7).&lt;/p&gt;

&lt;p&gt;The following assumptions are common to all 10 cells.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prices&lt;/td&gt;
&lt;td&gt;List prices read on 2026-08-09 (on-demand, excluding tax). I re-read the 2026-08-09 record with the Price List API (read-only) on 2026-10-16, and the values were the same. It covers 10 items and does not include the two PUT request prices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored volume&lt;/td&gt;
&lt;td&gt;20,480 GiB (20 TiB). Common to all cells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;Single-AZ first generation, 128 MBps. Storage efficiency 40% on the SSD tier and 20% on the capacity pool tier. 30% of the data on the capacity pool (common to all patterns). Reads are served from the file system, and writes are charged as PUT through S3 Access Points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Files&lt;/td&gt;
&lt;td&gt;Import threshold 128 KiB. For sizes below the threshold, the whole stored volume is assumed to be held as active in high-performance storage. Expiry is the default 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#a-common-baseline--20-tib-under-the-same-terms" rel="noopener noreferrer"&gt;Common baseline&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost figure 1: monthly cost of P1 and P4, which have no writes
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fceadr435nphp3eemdsui.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fceadr435nphp3eemdsui.png" alt="Monthly cost breakdown for P1 and P4, which have no writes. For each of P1 and P4, and for two sizes, 64 KiB and 4 MiB, three bars are shown side by side: reading directly with the S3 API, via S3 Files, and FSx for ONTAP. At 64 KiB, via S3 Files is $7,928.32 and $8,755.20, and FSx for ONTAP is $1,783.11 and $1,818.50. At 4 MiB, via S3 Files is $512.14 and $514.74, and FSx for ONTAP is $1,781.28 and $1,781.83. The horizontal axis runs from 0 to $12,500 and is shared with Figure 2" width="800" height="1537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cost figure 1: breakdown of the monthly cost of P1 and P4, which have no writes (ap-northeast-1, list price x quantity, not an invoice). The horizontal axis is shared with cost figure 2. The working-set ratio and read count of P1 are placeholders with no source. "Figure 1" and "Figure 2" inside the images mean these two cost figures, not the internals figure in a preceding chapter. The figure shows the same numbers as the table below (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/_assets/images/s3burst-s3files-cost-structure-en-dark@2x.png" rel="noopener noreferrer"&gt;dark theme version&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern / size&lt;/th&gt;
&lt;th&gt;Monthly operations&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;S3 Files high-performance storage&lt;/th&gt;
&lt;th&gt;Fixed bandwidth&lt;/th&gt;
&lt;th&gt;Requests etc.&lt;/th&gt;
&lt;th&gt;Of which S3 Files metadata charges&lt;/th&gt;
&lt;th&gt;Main-item subtotal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 64 KiB&lt;/td&gt;
&lt;td&gt;16,777,216 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$6.21&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$518.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 64 KiB&lt;/td&gt;
&lt;td&gt;16,777,216 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$43.52&lt;/td&gt;
&lt;td&gt;$2.56&lt;/td&gt;
&lt;td&gt;$7,928.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 64 KiB&lt;/td&gt;
&lt;td&gt;16,777,216 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$1.86&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,783.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 4 MiB&lt;/td&gt;
&lt;td&gt;262,144 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$512.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 4 MiB&lt;/td&gt;
&lt;td&gt;262,144 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0.14&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;td&gt;$512.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 4 MiB&lt;/td&gt;
&lt;td&gt;262,144 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$0.03&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,781.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$124.15&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$636.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$870.40&lt;/td&gt;
&lt;td&gt;$51.20&lt;/td&gt;
&lt;td&gt;$8,755.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$37.25&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,818.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1.94&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$513.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$2.74&lt;/td&gt;
&lt;td&gt;$0.80&lt;/td&gt;
&lt;td&gt;$514.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$0.58&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,781.83&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p1--64-kib-objects" rel="noopener noreferrer"&gt;P1 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p1--4-mib-objects" rel="noopener noreferrer"&gt;P1 / 4 MiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p4--64-kib-objects" rel="noopener noreferrer"&gt;P4 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p4--4-mib-objects" rel="noopener noreferrer"&gt;P4 / 4 MiB&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Requests etc." is the sum of S3 requests, S3 Files read and write charges (metadata charges included), and FSx for ONTAP capacity pool read requests and PUT. It is the purple category in the figure. "Storage" for FSx for ONTAP is SSD and capacity pool storage added together.&lt;/p&gt;

&lt;p&gt;The FSx for ONTAP rows assume reads are served from the file system, and they do not include read charges through S3 Access Points. The direct-read rows for the S3 API appear only for P1 and P4, which have no writes. In the patterns with writes, the write path differs by setup (S3 API PUT, NFS write plus sync, PUT through S3 Access Points), and putting them in one table would mix the write assumptions.&lt;/p&gt;

&lt;p&gt;In the 4 MiB rows, the S3 Files subtotal is nearly the same as the direct S3 API read subtotal ($512.14 vs. $512.10 for P1, and $514.74 vs. $513.94 for P4). 4 MiB is at or above the threshold, so the data is not held in high-performance storage and is read straight from the bucket. In the 64 KiB rows, S3 Files adds $7,372.80 of high-performance storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost figure 2: monthly cost of P5, P6, and P8, which have writes
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ibcw7p1emk6jsy05675.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ibcw7p1emk6jsy05675.png" alt="Monthly cost breakdown for P5, P6, and P8, which have writes. For two sizes, 64 KiB and 4 MiB, two bars are shown side by side: via S3 Files and FSx for ONTAP. At 64 KiB, via S3 Files is $8,875.46, $12,030.81, and $11,002.59, and FSx for ONTAP is $1,836.61, $2,218.13, and $2,147.36. At 4 MiB, via S3 Files is $516.04, $543.52, and $538.32, and FSx for ONTAP is $1,782.11, $1,788.08, and $1,786.97. The horizontal axis runs from 0 to $12,500 and is shared with Figure 1" width="800" height="1667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cost figure 2: breakdown of the monthly cost of P5, P6, and P8, which have writes (ap-northeast-1, list price x quantity, not an invoice). The horizontal axis is shared with cost figure 1. The write volume of P5 and the working-set ratio, read count, and write volume of P6 and P8 are placeholders with no source. The figure shows the same numbers as the table below (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/_assets/images/s3burst-s3files-cost-structure-writes-en-dark@2x.png" rel="noopener noreferrer"&gt;dark theme version&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern / size&lt;/th&gt;
&lt;th&gt;Monthly operations&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;S3 Files high-performance storage&lt;/th&gt;
&lt;th&gt;Fixed bandwidth&lt;/th&gt;
&lt;th&gt;Requests etc.&lt;/th&gt;
&lt;th&gt;Of which S3 Files metadata charges&lt;/th&gt;
&lt;th&gt;Main-item subtotal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, 16,777,216 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$990.66&lt;/td&gt;
&lt;td&gt;$55.68&lt;/td&gt;
&lt;td&gt;$8,875.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, 16,777,216 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$55.36&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,836.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, 262,144 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$4.04&lt;/td&gt;
&lt;td&gt;$0.87&lt;/td&gt;
&lt;td&gt;$516.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, 262,144 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$0.87&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,782.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 64 KiB&lt;/td&gt;
&lt;td&gt;671,088,640 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$4,146.01&lt;/td&gt;
&lt;td&gt;$192.00&lt;/td&gt;
&lt;td&gt;$12,030.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 64 KiB&lt;/td&gt;
&lt;td&gt;671,088,640 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$436.88&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$2,218.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 4 MiB&lt;/td&gt;
&lt;td&gt;10,485,760 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$31.52&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;$543.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 4 MiB&lt;/td&gt;
&lt;td&gt;10,485,760 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$6.83&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,788.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 64 KiB&lt;/td&gt;
&lt;td&gt;33,554,432 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$3,117.79&lt;/td&gt;
&lt;td&gt;$94.72&lt;/td&gt;
&lt;td&gt;$11,002.59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 64 KiB&lt;/td&gt;
&lt;td&gt;33,554,432 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$366.11&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$2,147.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 4 MiB&lt;/td&gt;
&lt;td&gt;524,288 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$26.32&lt;/td&gt;
&lt;td&gt;$1.48&lt;/td&gt;
&lt;td&gt;$538.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 4 MiB&lt;/td&gt;
&lt;td&gt;524,288 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$5.72&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,786.97&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p5--64-kib-objects" rel="noopener noreferrer"&gt;P5 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p5--4-mib-objects" rel="noopener noreferrer"&gt;P5 / 4 MiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p6--64-kib-objects" rel="noopener noreferrer"&gt;P6 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p6--4-mib-objects" rel="noopener noreferrer"&gt;P6 / 4 MiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p8--64-kib-objects" rel="noopener noreferrer"&gt;P8 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p8--4-mib-objects" rel="noopener noreferrer"&gt;P8 / 4 MiB&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding writes adds, on the S3 Files side, PUT to the S3 bucket and, at 64 KiB, import writes to high-performance storage. On the FSx for ONTAP side it adds PUT through S3 Access Points. For P6 at 64 KiB, "Requests etc." is $4,146.01 via S3 Files and $436.88 on FSx for ONTAP. Across the three patterns of cost figure 2, the 4 MiB subtotals stay within $516.04 to $543.52 via S3 Files and $1,782.11 to $1,788.08 on FSx for ONTAP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Items that push up the S3 Files subtotal at 64 KiB: high-performance storage and the number of writes
&lt;/h3&gt;

&lt;p&gt;S3 Files imports files below the threshold (default 128 KiB) into high-performance storage and charges $0.36 / GB-month for the amount held. Files at or above the threshold are not held and are read straight from the bucket. The model assumes the whole stored volume (20,480 GiB) is active at the small size. That makes high-performance storage $7,372.80 in all five 64 KiB cells and $0.00 in all five 4 MiB cells.&lt;/p&gt;

&lt;p&gt;On the FSx for ONTAP side, storage of $1,665.28 and fixed bandwidth of $115.97 are the same in all 10 cells. Only the "Requests etc." column moves with size and with read and write volume.&lt;/p&gt;

&lt;p&gt;Why the S3 Files subtotal is higher at 64 KiB splits into two cases by pattern. In P1, P4, and P5, the S3 Files subtotal falls below FSx for ONTAP once the $7,372.80 of high-performance storage is removed. In P6 and P8 it stays higher even after the removal. Those two write 335,544,320 objects, and the S3 bucket PUT price ($0.0047 / 1,000) is higher than the PUT price through S3 Access Points ($0.00108 / 1,000), which also contributes. Those two PUT prices are still the values read on 2026-08-09 and were not part of the 2026-10-16 re-read.&lt;/p&gt;

&lt;p&gt;The high-performance storage amount follows from assuming a 100% active share. That share is a placeholder, not a measured value. I made one observation of a related quantity (described in the section on assumptions under which the order can flip).&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata charges: the accounting assumption and one observation
&lt;/h3&gt;

&lt;p&gt;The model counts S3 Files metadata as 4 KiB x operations, in separate import-side and read-side lines, and includes both in the subtotal. The column "Of which S3 Files metadata charges" in the tables is the sum of those two lines, and across all 32 cells it matches usage x unit price ($0.04 / $0.07). In two measurements, on 2026-10-16 and 2026-10-17, the Cost Explorer usage for &lt;code&gt;APN1-Files-Read&lt;/code&gt; matched the sum of CloudWatch &lt;code&gt;DataReadBytes&lt;/code&gt; and &lt;code&gt;MetadataReadBytes&lt;/code&gt; to within 0.5% (daily, &lt;code&gt;Estimated: true&lt;/code&gt;). Ingest was billed as &lt;code&gt;APN1-Files-Write&lt;/code&gt; and did not appear in the CloudWatch write metrics. Because cost is set by usage (bytes), the per-operation breakdown of a read (open, getattr, and so on) does not change the estimate. The invoiced amount is offset to a net zero by credits, so I have not confirmed it as a billed figure. I did not measure other sizes or other instance types. The primary record of this reconciliation is at &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/s3files-billing-reconciled.md" rel="noopener noreferrer"&gt;billing reconciliation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I have one observation (2026-10-16, ap-northeast-1, m5.large, not reproduced). I read 1,000 4 MiB objects once each with &lt;code&gt;dd&lt;/code&gt;, and CloudWatch &lt;code&gt;MetadataReadBytes&lt;/code&gt; registered about 6.0 times per object, with an average of about 4.1 KiB each. Changing the I/O size to 1 MiB or 4 MiB made no difference. The 64 KiB control with the same procedure registered about 2.0 times. I do not know where the roughly 6 comes from, so I have not applied that count to the "4 KiB x operations" count in the model. &lt;code&gt;DataReadBytes&lt;/code&gt; for 4 MiB had 0 data points, so I could not confirm whether the reads went through NFS or directly to the bucket. For the ingest of 4 MiB objects written through the S3 API, &lt;code&gt;MetadataWriteBytes&lt;/code&gt; had 0 data points. I cannot tell whether no ingest happened or whether it is not metered.&lt;/p&gt;

&lt;p&gt;The roughly 6.0 figure is the number of data points the CloudWatch metric recorded. I later queried Cost Explorer and reconciled it against the billed usage. The &lt;code&gt;APN1-Files-Read&lt;/code&gt; usage on 2026-10-16 and 10-17 matched the sum of CloudWatch &lt;code&gt;DataReadBytes&lt;/code&gt; and &lt;code&gt;MetadataReadBytes&lt;/code&gt; to within 0.5% (daily, &lt;code&gt;Estimated: true&lt;/code&gt;). I did not capture the breakdown of the roughly 6.0 operations, but because cost is set by usage (bytes), the breakdown does not change the subtotal.&lt;/p&gt;

&lt;p&gt;The following remain unconfirmed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The invoiced amount itself. It is offset to a net zero by credits, so I have not confirmed it as a billed figure.&lt;/li&gt;
&lt;li&gt;Other instance types, nconnect, multiple processes, and other object sizes. I did not measure them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The metadata column in the tables keeps the assumption of one operation per read. The difference in the operation count does not affect a cost that is set by usage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assumptions under which the order can flip: what to replace with your values
&lt;/h3&gt;

&lt;p&gt;The order in this chapter is a result under fixed assumptions. If you replace the following assumptions with values from your environment, the amounts and the order move. The table of subtotals when one assumption moves at a time and the table of values at which the order flips are in the section "&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#baseline-assumptions-and-sensitivity" rel="noopener noreferrer"&gt;Baseline assumptions and sensitivity&lt;/a&gt;" of the generated document in the repository. A figure that takes P4 as an example and shows where each assumption sits and what kind of evidence backs it is in the section "&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#break-even-map-for-pattern-p4" rel="noopener noreferrer"&gt;Break-even map for pattern P4&lt;/a&gt;" (images: &lt;a href="https://raw.githubusercontent.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/_assets/images/s3burst-s3files-breakeven-map-en@2x.png" rel="noopener noreferrer"&gt;light&lt;/a&gt; and &lt;a href="https://raw.githubusercontent.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/_assets/images/s3burst-s3files-breakeven-map-en-dark@2x.png" rel="noopener noreferrer"&gt;dark&lt;/a&gt;). Line length does not represent uncertainty.&lt;/p&gt;

&lt;p&gt;Under the base assumptions, the FSx for ONTAP subtotal is lower at 64 KiB and the S3 Files subtotal is lower at 4 MiB. At 4 MiB, the direction does not change when one assumption moves at a time over the values tried, and it changes for every pattern in the scenario that moves several assumptions together toward a lower FSx for ONTAP subtotal (a combination that includes a value with no source). At 64 KiB, lowering the active share of S3 Files high-performance storage from the base makes the S3 Files subtotal lower, starting with the patterns that have fewer writes, though the base value of that share is a placeholder.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share of data on the capacity pool: fixed at 30% in every pattern.&lt;/li&gt;
&lt;li&gt;Storage efficiency of the SSD tier: fixed at 40%.&lt;/li&gt;
&lt;li&gt;Active share of S3 Files high-performance storage: fixed at 100%. A placeholder.&lt;/li&gt;
&lt;li&gt;S3 Files import threshold: the default 128 KiB.&lt;/li&gt;
&lt;li&gt;FSx for ONTAP deployment: Single-AZ first generation, 128 MBps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also retention items that the base leaves out. Bucket versioning is required, so the S3 Standard storage charge for the non-current versions you keep is added to the S3 Files side. Keeping 1,024 GiB as an example raises the S3 Files subtotal by $25.60 a month in all 10 cells of the tables ($0.025 per GiB-month, model output). For snapshots on the FSx for ONTAP side, I have not confirmed which tier they occupy or how they are billed, so you can enter a capacity but it does not enter the subtotal.&lt;/p&gt;

&lt;p&gt;I made one observation of a quantity close to the active share (2026-10-17, ap-northeast-1, 200,000 files of 64 KiB, the default import threshold). When I read 5% and 20% of the working set, the increase in &lt;code&gt;StorageBytes&lt;/code&gt;, the billing dimension of high-performance storage, was 1.094 times the bytes read (5.47% and 21.89% of the stored volume). The working-set ratios are values the experiment set. &lt;code&gt;StorageBytes&lt;/code&gt; is the sum of data and metadata, so it is not a direct measurement of the active share. In the same experiment &lt;code&gt;ImportFailures&lt;/code&gt; registered about 200,000 per file system. I did not investigate the cause and did not separate its effect on &lt;code&gt;StorageBytes&lt;/code&gt;. I have not confirmed the billed amount, so the ratio cannot be read as a dollar figure. It is one observation with no repeat in another environment, so I did not lower the 100% in the tables. The record is in the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/ja/verification/s3files-active-ratio-measured.md" rel="noopener noreferrer"&gt;verification record&lt;/a&gt; (Japanese).&lt;/p&gt;

&lt;p&gt;The capacity pool share and the active share of high-performance storage are fixed values decoupled from the working-set ratio. In P1, P4, and P5 the data touched within the month (the working set plus writes) does not reach the whole stored volume, so the two shares can be set again to match the volume actually touched. Fitting the active share of high-performance storage would lower the S3 Files subtotal at 64 KiB. For the FSx for ONTAP subtotal when the capacity pool share is fitted, the generated document only says that the two together would go down, and I have not checked each one separately. In P6 and P8 the data touched within the month adds up to the stored volume or more, so if the capacity pool share is assumed to cover the whole volume, the FSx for ONTAP subtotal would rise. The directions come from the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/14683a73308a9d2563c3cc6544da1c3f59cce03c/docs/en/reference/comparison/finops-s3-vs-s3ap.md#a-common-baseline--20-tib-under-the-same-terms" rel="noopener noreferrer"&gt;generated document&lt;/a&gt;, and I have not calculated the change in amounts. One more point: for S3 Files at 64 KiB in P1 and P4, which have no writes, the model counts no first-read stream (the GET from the bucket and the import write into high-performance storage) and counts all reads as high-performance storage reads. Counting it would add some items and remove others, and the model does not settle the net direction.&lt;/p&gt;

&lt;p&gt;The capacity pool read requests included in "Requests etc." for FSx for ONTAP assume one request per GET on the 30% of reads that hit the capacity pool. The actual count is unconfirmed.&lt;/p&gt;

&lt;p&gt;An observation that helps comes from an adjacent repository (the FSx for ONTAP monitoring integrations). On a file system of the same type (Single-AZ first generation, 128 MBps, SSD 1,024 GiB), on 2026-10-11, I read volumes that had been created with ALL and written to directly, from a Lambda function outside the VPC. CloudWatch &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; came to 0 per object at 64 KiB, about 5.4 to 5.6 at 1 MiB, and about 29.8 to 33.2 at 5 MiB (source: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Observability-integrations/blob/main/docs/ja/s3ap-throughput-benchmark.md#%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%94%E3%81%A8%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%91%E3%82%B7%E3%83%86%E3%82%A3%E3%83%97%E3%83%BC%E3%83%AB%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8A%E5%B0%8E%E5%87%BA%E5%80%A4" rel="noopener noreferrer"&gt;per-invocation reads&lt;/a&gt;, Japanese). The 1 MiB and 5 MiB values are for cases where operations registered, and 2 of the 8 runs built the same way registered none. The sizes measured were 64 KiB, 1 MiB, and 5 MiB; 4 MiB was not measured. What was counted is the CloudWatch operation count, not a billed count, and that record also leaves the relation between the two unconfirmed. The measurement date, the volume, and the client all differ from this post's measurements. For these reasons I did not apply these operation counts to the model's count (one request per GET). The capacity pool read request amount that follows from the count the model assumed is $0.03 to $1.16 across the five 4 MiB cells.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading data on the capacity pool: billing items exist, latency is unmeasured
&lt;/h3&gt;

&lt;p&gt;Reading an object on the capacity pool of a Tiering All volume through S3 Access Points can incur two billing items: GET through S3 Access Points and capacity pool read requests (documented). This post's measurements did not produce the number of requests per GetObject or the latency of reading from the capacity pool. The capacity pool storage price is lower than SSD, $0.0238 vs. $0.15 per GB-month (Single-AZ, ap-northeast-1, read 2026-08-09).&lt;/p&gt;

&lt;p&gt;These are the p50 observations of GET through S3 Access Points from a local machine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object size&lt;/th&gt;
&lt;th&gt;SSD side&lt;/th&gt;
&lt;th&gt;After changing to Tiering All&lt;/th&gt;
&lt;th&gt;p50 difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;64 KiB&lt;/td&gt;
&lt;td&gt;46.5 ms&lt;/td&gt;
&lt;td&gt;56.4 ms&lt;/td&gt;
&lt;td&gt;+9.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB&lt;/td&gt;
&lt;td&gt;58.6 ms&lt;/td&gt;
&lt;td&gt;74.0 ms&lt;/td&gt;
&lt;td&gt;+15.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 MiB&lt;/td&gt;
&lt;td&gt;116.0 ms&lt;/td&gt;
&lt;td&gt;121.3 ms&lt;/td&gt;
&lt;td&gt;+5.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The environment was 2026-10-10, ap-northeast-1, FSx for ONTAP Single-AZ first generation, 128 MBps, SSD 1,024 GiB, from a local machine outside AWS over the internet (round-trip time included), concurrency 1. It is one measurement: 166 to 407 requests on the SSD side and 10 requests after the change.&lt;/p&gt;

&lt;p&gt;I cannot read this difference as a delay from reading the capacity pool, for three reasons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the read window after the change, the per-volume &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; was 0. The reads did not reach the capacity pool.&lt;/li&gt;
&lt;li&gt;The 5 MiB difference (+5.3 ms) is smaller than the spread from p50 to p90 on the SSD side (9.0 ms).&lt;/li&gt;
&lt;li&gt;I did not alternate SSD-side and post-change reads, and the request counts are asymmetric.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L243-L247" rel="noopener noreferrer"&gt;Reads by tier&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;Before you read much Tiering All data through S3 Access Points, look at the per-volume &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; one-minute values in your own environment and then estimate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing between S3 bucket first and file system first
&lt;/h3&gt;

&lt;p&gt;Laid out at the same granularity, the constraints of the two setups are as follows. For monthly cost, treat the rows as guidance under the base assumptions of the estimate above.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Fits when&lt;/th&gt;
&lt;th&gt;Constraints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Files (S3 bucket first)&lt;/td&gt;
&lt;td&gt;You want the source of truth in an S3 bucket. Clients are Linux on AWS and speak NFSv4.1 or later. Objects of 128 KiB or more dominate. You do not want a fixed cost&lt;/td&gt;
&lt;td&gt;Bucket versioning required. Linux on AWS only. NFSv4.1 / 4.2 only (NFSv3, SMB, and nconnect unsupported). About 60 seconds from file to bucket. Objects below 128 KiB incur high-performance storage charges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for ONTAP + S3 Access Points (file system first)&lt;/td&gt;
&lt;td&gt;You want the source of truth in an ONTAP volume. You have NFSv3 or SMB clients. You read from outside AWS by mounting over NFS or SMB (I did not measure this). Objects below 128 KiB dominate&lt;/td&gt;
&lt;td&gt;A fixed cost of $269.57 a month or more. Capacity pool read latency is unmeasured, and read request charges can apply. Clients share the throughput capacity. S3 event notifications, lifecycle, and versioning are not supported on the S3 Access Points side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your clients are Linux on AWS and you handle objects of 128 KiB or more while keeping the source of truth in an S3 bucket, start evaluating with S3 Files. If you have NFSv3 or SMB clients, read from outside AWS by mounting over NFS or SMB (I did not measure this), or mostly have objects below 128 KiB, start with FSx for ONTAP and S3 Access Points.&lt;/p&gt;

&lt;p&gt;The object-size conditions are guidance drawn from the 64 KiB and 4 MiB results I estimated. I did not estimate sizes near 128 KiB. Decide the monthly order only after you replace the values in "Assumptions under which the order can flip" with those of your environment and check.&lt;/p&gt;

&lt;h2&gt;
  
  
  An invalid warm / cold comparison: lowering the threshold does not evict data already loaded
&lt;/h2&gt;

&lt;p&gt;My first warm / cold comparison was invalid because both reads were warm. I have not confirmed the latency difference between warm and cold. Here warm means reading data held in high-performance storage, and cold means reading straight from the bucket.&lt;/p&gt;

&lt;p&gt;I measured once with a different design (2026-10-19, ap-northeast-1, one c5n.9xlarge, Amazon Linux 2023, 64 KiB files placed one per directory, &lt;code&gt;drop_caches&lt;/code&gt; before each read, default import threshold, concurrency 1). The p50 of a first read was 230.22 ms, and the p50 of re-reading the same file two minutes later was 18.71 ms (n=360 each). The paired difference on the same object was positive for all 360 pairs. But &lt;code&gt;DataReadBytes&lt;/code&gt; was the same 65,972 B for the first and second reads, so I cannot distinguish the path of the first read. Of 40 objects in one directory, only the first was 438.9 ms, and the median of the other 39 was 13.12 ms (one observation; I did not investigate the reason). The conditions differ from the 4.05 ms (straight from the bucket) later in this chapter, and I did not investigate the cause of the difference. Details are in the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/9e41dea3d516225b80cd70f7cd9bc54422965fc8/docs/ja/verification/s3files-warm-cold-latency-measured.md" rel="noopener noreferrer"&gt;verification record&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The goal was to check the claim that only frequently read data becomes low-latency. I assumed that lowering the import threshold (&lt;code&gt;sizeLessThan&lt;/code&gt;, default: import files under 128 KiB) and then reading the same file would make the read cold, and I measured on that assumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it was invalid: the threshold only decides what the next first access imports
&lt;/h3&gt;

&lt;p&gt;The threshold decides what gets imported the next time a directory is accessed for the first time. Data already in high-performance storage is not evicted when you lower it. Even in the window I meant to be cold, CloudWatch &lt;code&gt;DataReadBytes&lt;/code&gt; (bytes read from high-performance storage) was nonzero, so both reads came from high-performance storage.&lt;/p&gt;

&lt;p&gt;The numbers gave it away. The supposedly cold read was faster than the warm one, at 2.94 ms, which cannot be right. The record reads that difference as run-to-run variation and remount differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three reads: two are valid
&lt;/h3&gt;

&lt;p&gt;The environment was ap-northeast-1, 2026-10-10, c5n.9xlarge, 64 KiB, &lt;code&gt;o_direct&lt;/code&gt;, concurrency 1, p50 of n=30.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;Valid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High-performance storage (threshold 131072, 64 KiB already imported)&lt;/td&gt;
&lt;td&gt;5.33 ms&lt;/td&gt;
&lt;td&gt;Valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same file after lowering the threshold to 1024&lt;/td&gt;
&lt;td&gt;2.94 ms&lt;/td&gt;
&lt;td&gt;Invalid (both warm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A newly created 64 KiB file with the threshold left at 1024 (direct from the bucket)&lt;/td&gt;
&lt;td&gt;4.05 ms&lt;/td&gt;
&lt;td&gt;Valid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L175-L179" rel="noopener noreferrer"&gt;Three reads&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;The two valid rows, 5.33 ms and 4.05 ms, differ by an amount within run-to-run variation. A separate t3.small record showed 10.8 ms from high-performance storage and 55.0 ms straight from the bucket (2026-09-01), but that did not reproduce on c5n.9xlarge. The environments and methods differ, so it is not a comparison that varies only the path in one environment.&lt;/p&gt;

&lt;p&gt;To measure cold, read a new file that has not been imported after lowering the threshold, and confirm the path with &lt;code&gt;DataReadBytes&lt;/code&gt;. The 2.94 ms in this post cannot serve as evidence for a warm / cold difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status of the four stated values: I got numbers for two
&lt;/h2&gt;

&lt;p&gt;Of the four values AWS states, I got numbers for two: propagation time when using both files and the S3 API, and aggregate growth with client count (up to 8). The comparison for "only frequently read data becomes low-latency" was invalid, so I cannot confirm it. I did not measure "existing tools work as they are".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stated value&lt;/th&gt;
&lt;th&gt;What I confirmed&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Existing file-based tools work as they are&lt;/td&gt;
&lt;td&gt;The measurement tool, VDBENCH, ran on the mount. I did not look at compatibility with existing applications&lt;/td&gt;
&lt;td&gt;Not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same data is usable from both files and the S3 API&lt;/td&gt;
&lt;td&gt;Propagation p50 was 63,769.3 ms (a window of about 60 seconds) from writing a file to reading it with the S3 API, and 1,533.1 ms from writing with the S3 API to reading it as a file (t3.small, NFSv4.2, 64 B, concurrency 1, n=30, 2026-09-01)&lt;/td&gt;
&lt;td&gt;Measured. File to S3 has a window of about 60 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only frequently read data sits in high-performance storage and gets low latency&lt;/td&gt;
&lt;td&gt;64 KiB p50 was 5.33 ms from high-performance storage and 4.05 ms straight from the bucket (c5n.9xlarge, n=30, 2026-10-10), within run-to-run variation. The first comparison was invalid&lt;/td&gt;
&lt;td&gt;Not confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many clients can use it at once&lt;/td&gt;
&lt;td&gt;Aggregate 784.4 to 6,282.6 MB/s (1 to 8 clients, 8.01x, c5n.2xlarge, 2026-10-10). I did not measure the 25,000-connection and TB/s-class limits&lt;/td&gt;
&lt;td&gt;Measured up to 8 clients&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L42-L45" rel="noopener noreferrer"&gt;Propagation measurement&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L175-L179" rel="noopener noreferrer"&gt;Three reads&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L134-L139" rel="noopener noreferrer"&gt;Client-count measurement (same)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;The values come with costs. Propagation from file to bucket takes about 60 seconds, the supported protocols are only NFSv4.1 and NFSv4.2, bucket versioning is required, and clients are limited to Linux on AWS. The two confirmed points are usable as grounds for adoption. Treat the low-latency value as an assumption only after you measure a cold read in your own environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not measured
&lt;/h2&gt;

&lt;p&gt;This post's measurements cover a single S3 Files mount (c5n.9xlarge), S3 Files client count (c5n.2xlarge, 1 to 8 clients), S3 Files 64 KiB cold and warm latency (one client), and latency by tier (SSD vs. capacity pool) through FSx for ONTAP S3 Access Points for Single-AZ and the preferred AZ of Multi-AZ first generation, inside a VPC. I did not measure the following four things.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I have not measured (subject and object stated)&lt;/th&gt;
&lt;th&gt;Why not&lt;/th&gt;
&lt;th&gt;When not knowing hurts&lt;/th&gt;
&lt;th&gt;How to measure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;For S3 Files, I cannot distinguish whether the latency of a first read is a direct read from the bucket or a read from high-performance storage after import.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DataReadBytes&lt;/code&gt; was the same 65,972 B for the first and second reads, so this metric does not distinguish the two. The values I measured are in the chapter "An invalid warm / cold comparison".&lt;/td&gt;
&lt;td&gt;You use first-access latency as a direct-from-bucket value in design.&lt;/td&gt;
&lt;td&gt;Lower the import threshold below the file size, create new files, place them one per directory, and read each after a &lt;code&gt;drop_caches&lt;/code&gt;. Compare the latency in a window where &lt;code&gt;DataReadBytes&lt;/code&gt; does not rise against the first-read value in the body.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;For FSx for ONTAP S3 Access Points, I did not confirm how the CloudWatch read operation count corresponds to the billed request count. I also do not know the conditions under which an ALL volume's reads reach the capacity pool.&lt;/td&gt;
&lt;td&gt;I measured the operation count (the comparison table in the body). The Cost Explorer value on the measurement day (&lt;code&gt;APN1-Requests.SAZ_2N:CPoolStdRd&lt;/code&gt;) is daily and account-wide, read before aggregation completed. The account has another FSx for ONTAP, so I cannot compare it against the measured window's operation count. Batches where the reads did not reach, or only partly reached, the capacity pool numbered 2 / 12 at 4 MiB, 8 / 13 at 1 MiB, and 10 / 10 at 64 KiB (supplementary batches included). I did not investigate the cause.&lt;/td&gt;
&lt;td&gt;You take the operation count directly as the billed request count to estimate read cost on a Tiering All volume.&lt;/td&gt;
&lt;td&gt;After the date is settled, re-read the daily &lt;code&gt;CPoolStdRd&lt;/code&gt; value, subtract the same day's &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; (CloudWatch) of the other file systems, and compare against the measured file system. For the reach conditions, repeat with the time from write to read, the read method (ALL only, alternating), and the size separated.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I did not measure the latency difference of reading an SSD volume and a capacity pool volume through S3 Access Points from an on-premises client. The result from EC2 inside a VPC is in the comparison table in the body.&lt;/td&gt;
&lt;td&gt;I measured only from EC2 inside a VPC and did not set up a client outside AWS. The single reading from a local machine outside AWS over the internet (+5.3 to +15.4 ms) did not reach the capacity pool and cannot serve as an on-premises value.&lt;/td&gt;
&lt;td&gt;Your client is on premises and you apply the in-VPC difference as is.&lt;/td&gt;
&lt;td&gt;From an on-premises client, alternate reads of the same two volumes at the same size and confirm that the per-volume &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; rose in the read window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;For Multi-AZ first generation, I cannot distinguish whether latency from a standby-AZ client differs from a preferred-AZ client.&lt;/td&gt;
&lt;td&gt;The standby-AZ client had one batch per size (n=30), and the SSD side at 4 MiB was bimodal, making the difference negative. It cannot be read as an AZ difference.&lt;/td&gt;
&lt;td&gt;Your client is in an AZ other than the preferred one and you apply the preferred-AZ result.&lt;/td&gt;
&lt;td&gt;From a standby-AZ client, read several batches per size. Investigate the cause of the bimodal SSD side first.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If any row has conditions that differ from yours, measure that row first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it yourself: three runbooks from creation to teardown
&lt;/h2&gt;

&lt;p&gt;You can follow this post's measurements in your own AWS account with the same steps. I put runbooks from creating the environment to tearing it down on GitHub. The links are pinned to the commit of the measurement.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create the environment and mount: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/deployment/aws-s3files-compare.md" rel="noopener noreferrer"&gt;S3 Files verification environment setup&lt;/a&gt; (Japanese). It builds with CloudFormation and &lt;code&gt;runbook.sh&lt;/code&gt;. The most frequent cause of a silently hanging mount (missing CloudWatch permissions on the host role) is written at the start of these steps.&lt;/li&gt;
&lt;li&gt;Measure throughput and client count: from the section "Measuring throughput and client count" on the same page, go to &lt;code&gt;throughput-runbook.sh&lt;/code&gt; (single mount) and &lt;code&gt;s4-ladder.sh&lt;/code&gt; (1 to 8 clients). VDBENCH requires accepting Oracle's license, so obtain it separately.&lt;/li&gt;
&lt;li&gt;Results and conditions: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md" rel="noopener noreferrer"&gt;S3 Files file system performance&lt;/a&gt; (Japanese). This is where this post's numbers come from, each with its environment. It also states that each is a single sample.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leaving the verification environment up keeps billing. When you finish measuring, tear it down with the runbook's &lt;code&gt;destroy&lt;/code&gt; and &lt;code&gt;stop&lt;/code&gt;, then confirm by count that nothing remains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Two things hold from the measurements, and one from the estimate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The single-mount ceiling was one efs-proxy CPU core. It was 1,104.9 MB/s for 1 MiB sequential reads on a c5n.9xlarge (one run, 2026-10-10, ap-northeast-1).&lt;/li&gt;
&lt;li&gt;When bandwidth falls short, adding clients raised it. From 1 to 8 c5n.2xlarge clients, the aggregate went from 784.4 MB/s to 6,282.6 MB/s (8.01x). I have not measured 9 or more.&lt;/li&gt;
&lt;li&gt;In the estimate, under this model's base assumptions, the monthly cost order split by object size, not by read and write volume. The candidate for the dividing line is around the S3 Files import threshold (128 KiB), and I estimated only two sizes, 64 KiB and 4 MiB. With the stored volume fixed at 20 TiB and five patterns (P1, P4, P5, P6, P8), 64 KiB comes to $1,783.11 to $2,218.13 a month on FSx for ONTAP and $7,928.32 to $12,030.81 via S3 Files, and 4 MiB comes to $512.14 to $543.52 via S3 Files and $1,781.28 to $1,788.08 on FSx for ONTAP (ap-northeast-1, subtotals of the main line items with prices read on 2026-08-09; list price x quantity, not an invoice). This order is a result under the model's base assumptions and can flip when they change, so check it against your own values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which one to consider first narrows down by where the source of truth lives and where clients read from. Because the monthly order by object size can change with assumptions, treat the object-size conditions in the next table as guidance under the base assumptions of the estimate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Check first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The source of truth is an S3 bucket. Clients are Linux on AWS and can use NFSv4.1 or later. Objects of 128 KiB or more dominate&lt;/td&gt;
&lt;td&gt;S3 Files&lt;/td&gt;
&lt;td&gt;Bucket versioning, about 60 seconds from file to bucket, and that NFSv3 and SMB are not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The source of truth is an ONTAP volume. You have NFSv3 or SMB clients, read from outside AWS by mounting over NFS or SMB (I did not measure this), or mostly have small objects below 128 KiB&lt;/td&gt;
&lt;td&gt;FSx for ONTAP with S3 Access Points&lt;/td&gt;
&lt;td&gt;A fixed cost of $269.57 a month or more, capacity pool read request charges, and that clients share the throughput capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm / cold latency, 9 or more clients, or the capacity pool read count will decide your design&lt;/td&gt;
&lt;td&gt;Measure in your own environment, then decide&lt;/td&gt;
&lt;td&gt;For 9 or more clients, the end of the chapter "An aggregate that scales with client count"; otherwise the rows of the "What I have not measured" table whose conditions differ from yours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I keep every measured value, each with its environment, on &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I hope this helps anyone with a similar need (collect in S3, read as files). &amp;lt;!-- allow:ai-style the "I hope this helps" closing mirrors the JA 一助になれば幸いです and is the author's fixed first-person sign-off (global-author-voice); HTML comments are stripped on publish so the visible text is unchanged --&amp;gt;&lt;/p&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

&lt;p&gt;Yoshiki Fujiwara&lt;/p&gt;

</description>
      <category>s3files</category>
      <category>aws</category>
      <category>s3</category>
      <category>storage</category>
    </item>
    <item>
      <title>Why Amazon FSx for NetApp ONTAP block performance differed by about 2x on the same specified values, isolated by measurement</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:43:42 +0000</pubDate>
      <link>https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi</link>
      <guid>https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi</guid>
      <description>&lt;h1&gt;
  
  
  Why Amazon FSx for NetApp ONTAP block performance differed by about 2x on the same specified values, isolated by measurement
&lt;/h1&gt;

&lt;p&gt;While measuring, I hit a case where the templates and the specified values matched, yet the number&lt;br&gt;
moved every time I measured. Rather than let it go, I wanted to isolate whether it was a&lt;br&gt;
measurement mistake or something about the environment, so I eliminated the causes one at a time.&lt;/p&gt;

&lt;p&gt;The same thing shows up when a post-migration performance test does not agree, and when you want to&lt;br&gt;
settle on one representative value while sizing an initial build. It is a question that eats a lot of&lt;br&gt;
time, so this article is the record of that isolation.&lt;/p&gt;

&lt;p&gt;What this article does and does not cover: it covers investigating the cause of the performance&lt;br&gt;
swing, and how to isolate the bottleneck using four CloudWatch metrics. It does not cover mount&lt;br&gt;
procedures, how to confirm multipathing, or figures usable for sizing (details under "What this&lt;br&gt;
article does not cover" below). All figures are from a Single-AZ configuration; Multi-AZ is&lt;br&gt;
unverified.&lt;/p&gt;

&lt;p&gt;dev.to series: &lt;code&gt;FSx for ONTAP Block Protocols, Measured&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A 1 MiB sequential read on Amazon FSx for NetApp ONTAP (hereafter FSx for ONTAP), measured with the&lt;br&gt;
same template, the same specified values and the same workload, &lt;strong&gt;varied by 2.64x across&lt;br&gt;
environments.&lt;/strong&gt; Each one was stable within its own run. This is not measurement scatter.&lt;/p&gt;

&lt;p&gt;This is the record of eliminating the causes one at a time. It was not the path, not how the&lt;br&gt;
connection was made, not the queue count, not the distribution, and not the iopolicy (Linux's&lt;br&gt;
multipath setting for distributing I/O across paths to an NVMe device). &lt;strong&gt;What was&lt;br&gt;
acting on it was the layout on disk, which breaks after 300 seconds of 4 KiB random writes and&lt;br&gt;
comes back when the data is rewritten sequentially.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three times along the way I nearly settled on the wrong explanation. First: "if it exceeds the&lt;br&gt;
specified value it must be served from memory." Second: "the layout must also be setting the&lt;br&gt;
amplification factor for 4 KiB random reads." Third: "the amplification factor must be decided by&lt;br&gt;
whether 4 KiB random writes ever ran on that device." &lt;strong&gt;All three were disproved once I aimed a&lt;br&gt;
measurement that could decide them.&lt;/strong&gt; All three are in here. The third had five observations&lt;br&gt;
lined up cleanly behind it and survived until a control experiment killed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was measured
&lt;/h2&gt;

&lt;p&gt;Assuming no prior knowledge of FSx for ONTAP, here's what varies between the deployments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment 1&lt;/strong&gt;: AWS Cloud, single VPC, single AZ. One EC2 client -&amp;gt; (NVMe/TCP, 1 MiB
sequential read) -&amp;gt; Amazon FSx for NetApp ONTAP (same template, same specified values)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment 2&lt;/strong&gt;: same AWS Cloud, single VPC, single AZ. One EC2 client -&amp;gt; (same connection,
same workload) -&amp;gt; Amazon FSx for NetApp ONTAP (same template, same specified values)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The invisible difference between Deployment 1 and 2&lt;/strong&gt;: layout on disk. It never appears in
the template or the list of specified values&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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fb6b83cd3d6bc516f12478138cc36bb30a3ae0ec3%2Fdocs%2F_assets%2Fimages%2Fs3burst-block-c-layout-en%402x.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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fb6b83cd3d6bc516f12478138cc36bb30a3ae0ec3%2Fdocs%2F_assets%2Fimages%2Fs3burst-block-c-layout-en%402x.png" alt="Two FSx for ONTAP file systems built from the same template and the same specified values&lt;br&gt;
(Deployment 1, Deployment 2), each measured from one EC2 client with the same connection and the&lt;br&gt;
same workload. Shows what can be held identical (path, connection setup, queue count,&lt;br&gt;
distribution, iopolicy) against what cannot (layout on disk)" width="800" height="726"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: **matching the CloudFormation template and the specified values doesn't make the layout&lt;br&gt;
on disk match across a redeploy.&lt;/em&gt;* Path, how the connection is made, queue count, distribution,&lt;br&gt;
and iopolicy can all be held to the same value across deployments. Layout is the one condition&lt;br&gt;
that changes every deployment and never appears in the list of specified values.*&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Role in this article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment 1 / Deployment 2&lt;/td&gt;
&lt;td&gt;Separate FSx for ONTAP file systems, built from the same template and the same specified values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conditions that can be matched&lt;/td&gt;
&lt;td&gt;Path, how the connection is made, queue count, distribution, iopolicy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Condition that can't be matched&lt;/td&gt;
&lt;td&gt;Layout on disk. Breaks after 300 seconds of 4 KiB random writes, comes back when rewritten sequentially&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What this article does not cover. Mount procedure is out of scope&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/block-measurement-runbook.md" rel="noopener noreferrer"&gt;repository record, Japanese&lt;/a&gt;).&lt;br&gt;
Confirming whether multipathing is usable is also out of scope (&lt;a href="https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl"&gt;separate article&lt;/a&gt;).&lt;br&gt;
&lt;strong&gt;The figures in this article cannot be used for sizing either.&lt;/strong&gt; Why they cannot is the subject of&lt;br&gt;
the article. All figures are from a Single-AZ configuration; Multi-AZ is unverified.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One of three, but there is no reading order.&lt;/strong&gt; The block-protocol measurements are split by&lt;br&gt;
reader.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/aws-builders/moving-on-premises-block-storage-to-aws-the-divide-by-625-mbps-sizing-approach-comes-out-wrong-31pc"&gt;Sessions and queues&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;People about to mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl"&gt;Confirming whether multipathing is usable&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;People who followed the procedure and got stuck&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;What moves the numbers&lt;/strong&gt; (this article)&lt;/td&gt;
&lt;td&gt;People who measure the numbers, and people who cite them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;None of the three presupposes the other two.&lt;/strong&gt; Read only the part you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same property on the file-protocol side (NFS / SMB / S3 API) is covered in&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/amazon-s3-files-vs-amazon-fsx-for-netapp-ontap-two-designs-for-using-s3-as-a-file-s3-burst-part-4p9m"&gt;S3 Burst Part 2&lt;/a&gt;&lt;/strong&gt;, with the primary record of those figures in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md" rel="noopener noreferrer"&gt;throughput, IOPS and concurrency (Japanese)&lt;/a&gt;.&lt;br&gt;
Those figures are also &lt;code&gt;iorate=max&lt;/code&gt; values from this environment, so &lt;strong&gt;the way of measuring&lt;br&gt;
described here applies to them unchanged.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The environment measured
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File system&lt;/td&gt;
&lt;td&gt;FSx for ONTAP second generation SINGLE_AZ_2, 6,144 MBps x 1 HA pair, SSD 4,096 GiB, provisioned 200,000 IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1, single AZ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not one version.&lt;/strong&gt; It spans 9.18.1P5 (the early deployments), 9.18.1P6 (the per-path measurements) and 9.18.1 (the last three deployments). &lt;strong&gt;This is exactly the unresolved candidate left at the end of this article&lt;/strong&gt; (the cross-article version mapping is in the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-ontap-version-matrix.md" rel="noopener noreferrer"&gt;ONTAP version matrix&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVMe read cache&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Disabled&lt;/strong&gt; (confirmed on both nodes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LUN / namespace&lt;/td&gt;
&lt;td&gt;600 GiB, on a 900 GiB volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before measuring&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;All 600 GiB was written once.&lt;/strong&gt; Unwritten blocks on a thin-provisioned volume return zeros, so reading without writing first measures nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;c5n.9xlarge; 50 Gbps network is the guaranteed figure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instrument&lt;/td&gt;
&lt;td&gt;VDBENCH 5.04.07 directly, &lt;strong&gt;&lt;code&gt;iorate=max&lt;/code&gt; (unlimited)&lt;/strong&gt;, 512 threads, 60 s warm-up + 300 s measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test data / efficiency settings&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not recorded whether the VDBENCH payload was compressible, or whether volume inline efficiency (&lt;code&gt;StorageEfficiencyEnabled&lt;/code&gt;) was enabled.&lt;/strong&gt; This article's subject is on-disk layout, so whether efficiency settings affected results is an unconfirmed variable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The window is 300 seconds, so the figures in this article include burst. They cannot be cited as a&lt;br&gt;
baseline.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every figure in this article is an &lt;code&gt;iorate=max&lt;/code&gt; saturation point, not an operating point.&lt;/strong&gt;&lt;br&gt;
No target IOPS was given; it is what the path would take. &lt;strong&gt;This distinction bears on the subject&lt;br&gt;
of the article itself.&lt;/strong&gt; A saturation point answers "where does it jam", and does not answer "at&lt;br&gt;
what response time can it be used".&lt;/p&gt;

&lt;p&gt;The file-protocol side of the same repository measured that gap. With a target of 4,400 IOPS the&lt;br&gt;
result was 4,406 MB/s at &lt;strong&gt;8.86 ms&lt;/strong&gt;; unlimited, it was 4,204 MB/s at &lt;strong&gt;121.79 ms&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;Unlimited is 5% lower in throughput and 14x worse in response time.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Recording one &lt;code&gt;iorate=max&lt;/code&gt; point as "the ceiling" leaves behind a figure that is lower than what&lt;br&gt;
is reachable and an order of magnitude worse in response time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response time was recorded as measured in only the two series under "not concurrency" below.&lt;/strong&gt;&lt;br&gt;
Elsewhere the values VDBENCH printed were not kept, and the environments are torn down. But with&lt;br&gt;
512 threads fixed and no rate limit, response time is the identity &lt;code&gt;512 / IOPS&lt;/code&gt; (derived 216.6 ms&lt;br&gt;
against measured 216.588 ms, confirmed to agree). &lt;strong&gt;The figures below can be read through that&lt;br&gt;
identity&lt;/strong&gt;: 1,288.87 MB/s is about &lt;strong&gt;397 ms&lt;/strong&gt;, 3,407.60 MB/s about &lt;strong&gt;150 ms&lt;/strong&gt;, 546.02 MB/s about&lt;br&gt;
&lt;strong&gt;937 ms&lt;/strong&gt;. &lt;strong&gt;All three are derived, not measured.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The response-time-versus-load curve was not measured.&lt;/strong&gt; The file-protocol side produced a curve&lt;br&gt;
across target IOPS values and a cutoff (the IOPS at which a given latency is reached) with&lt;br&gt;
&lt;a href="https://github.com/shuichi-taketani/auto_vdbench" rel="noopener noreferrer"&gt;auto_vdbench&lt;/a&gt;, but &lt;strong&gt;the same thing could not be&lt;br&gt;
produced on the block side.&lt;/strong&gt; auto_vdbench creates test files on a mount path and cannot drive a&lt;br&gt;
raw device or an NVMe namespace. &lt;strong&gt;Open work.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The same configuration varied widely
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is the part of the article I would most like read carefully.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As F-3, the same shape was measured three times within one connection.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st&lt;/td&gt;
&lt;td&gt;3,401.49 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2nd&lt;/td&gt;
&lt;td&gt;3,407.60 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3rd&lt;/td&gt;
&lt;td&gt;3,407.59 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Within one connection it reproduces to 0.18%.&lt;/strong&gt; The second and third differ by 0.01 MB/s.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;three environments built with the same procedure, the same template and the same negotiated&lt;br&gt;
result (4 queues)&lt;/strong&gt; produced different values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;th&gt;Stability within the window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First&lt;/td&gt;
&lt;td&gt;1,288.87 MB/s&lt;/td&gt;
&lt;td&gt;stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second&lt;/td&gt;
&lt;td&gt;2,366.49 MB/s&lt;/td&gt;
&lt;td&gt;stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third&lt;/td&gt;
&lt;td&gt;3,401-3,408 MB/s&lt;/td&gt;
&lt;td&gt;stable (0.18%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The spread is 2.64x.&lt;/strong&gt; Each was stable within its run, so this is not measurement scatter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So you cannot size from a single figure of the form "NVMe/TCP throughput on this configuration is&lt;br&gt;
N MB/s".&lt;/strong&gt; At the point where I had measured the second environment, I was about to write 2,366.49&lt;br&gt;
as the representative value. I had not followed my own rule of repeating three times, so I noticed&lt;br&gt;
one environment later than I should have.&lt;/p&gt;

&lt;p&gt;I had put forward one candidate at the time: Asymmetric Namespace Access (the mechanism that&lt;br&gt;
tells the host which of several paths to the same namespace is the shortest one, referred to as&lt;br&gt;
multipathing throughout this article) is unusable, so&lt;br&gt;
which of the two controllers the device under measurement sits behind can change per deployment.&lt;br&gt;
&lt;strong&gt;Measurement said otherwise.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The path candidate is gone
&lt;/h3&gt;

&lt;p&gt;I brought in a kernel with multipathing enabled (Rocky Linux 9.7) and measured &lt;strong&gt;within one deployment, on&lt;br&gt;
one namespace, with the paths narrowed to one at a time.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;optimized only (the node holding the namespace)&lt;/td&gt;
&lt;td&gt;1,278.11 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;non-optimized only&lt;/td&gt;
&lt;td&gt;1,279.07 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;0.08% apart. Either controller gives the same figure, so &lt;strong&gt;the path cannot explain that spread.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How the connection is made was eliminated too. Connecting from optimized first, from non-optimized&lt;br&gt;
first, and using &lt;code&gt;connect-all&lt;/code&gt; as the AWS procedure has it, gave 1,276.60 / 1,277.38 / 1,277.28&lt;br&gt;
MB/s. &lt;strong&gt;A spread of 0.06%.&lt;/strong&gt; The queue count was the same in every configuration&lt;br&gt;
(&lt;code&gt;queue_count=5&lt;/code&gt;, 10 established sockets to &lt;code&gt;:4420&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  What was acting on it was when the data being read was last written
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Within one deployment, one device, one path and one policy, rewriting the 600 GiB brings it back.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Immediately before the rewrite (control)&lt;/td&gt;
&lt;td&gt;1,239.15 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Immediately after the rewrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,298.54 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Once more, right after that&lt;/td&gt;
&lt;td&gt;2,296.12 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;1.85x. &lt;strong&gt;And the second read does not fall back. This is not a property that is consumed by one&lt;br&gt;
read.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the only place in this article where a ratio is written.&lt;/strong&gt; The numerator and denominator&lt;br&gt;
come from the same file system, the same device, the same path and the same policy: the reason a&lt;br&gt;
ratio can be written here is the same reason none is written in the other sections.&lt;/p&gt;

&lt;p&gt;CloudWatch answers what the cold side is running into.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;&lt;code&gt;DiskIopsUtilization&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Disk throughput utilization&lt;/th&gt;
&lt;th&gt;Network utilization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold read&lt;/td&gt;
&lt;td&gt;avg 57-60 / &lt;strong&gt;max 95.0-95.4&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;30 / 42&lt;/td&gt;
&lt;td&gt;8.7 / 11.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm read&lt;/td&gt;
&lt;td&gt;avg &lt;strong&gt;13.6-14.4&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;40 / 41&lt;/td&gt;
&lt;td&gt;19.2 / 19.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ceiling on the cold side is SSD IOPS. It is not the 6,144 MBps of throughput capacity that&lt;br&gt;
was specified (utilization 42% or below). It is not the network either (11% or below).&lt;br&gt;
&lt;strong&gt;And at 2,298 MB/s on the warm side, none of the utilization figures FSx for ONTAP publishes is at&lt;br&gt;
its ceiling.&lt;/strong&gt; What is acting on it there is not known.&lt;br&gt;
(What is being read here is &lt;code&gt;DiskIopsUtilization&lt;/code&gt; / disk throughput utilization / network&lt;br&gt;
utilization from the&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/so-file-system-metrics.html" rel="noopener noreferrer"&gt;second-generation file system metrics&lt;/a&gt;.&lt;br&gt;
The utilization metrics emit one point per HA pair and per aggregate.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Not reads, and not elapsed time
&lt;/h3&gt;

&lt;p&gt;Since it moves with "when it was written", I aimed separate measurements at &lt;strong&gt;what brings it down.&lt;/strong&gt;&lt;br&gt;
On top of one fill, reads, elapsed time and writes were applied in turn.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Immediately after&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The fill&lt;/td&gt;
&lt;td&gt;2,263.8 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A second read straight after&lt;/td&gt;
&lt;td&gt;2,267.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A third read straight after&lt;/td&gt;
&lt;td&gt;2,268.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;30 minutes idle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,258.5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;300 seconds of 4 KiB random writes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,272.8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refilling&lt;/td&gt;
&lt;td&gt;2,235.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads do not bring it down.&lt;/strong&gt; Three full passes over 600 GiB, a spread of 0.2%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time does not bring it down.&lt;/strong&gt; 30 minutes with nothing running, within 0.4%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4 KiB random writes bring it down.&lt;/strong&gt; 2,258.5 to 1,272.8, a fall of 1.77x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not generalize this to "writes bring it down". What was aimed was one point: 300 seconds of&lt;br&gt;
4 KiB random writes. &lt;strong&gt;The result of separating shape from duration and aiming again is in&lt;br&gt;
the section below, and the generalization&lt;br&gt;
did not hold.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What falls is not cache but the layout on disk
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;I first explained this as "cache retention". That was wrong.&lt;/strong&gt; Read CloudWatch interval by&lt;br&gt;
interval and the mechanism comes out plainly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;One read on the disk side&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Disk IOPS&lt;/th&gt;
&lt;th&gt;Against the specified 200,000&lt;/th&gt;
&lt;th&gt;&lt;code&gt;DiskRead / DataRead&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right after the fill (5 intervals)&lt;/td&gt;
&lt;td&gt;2,219-2,263 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;87.3-89.6 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25,743-27,404&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;97-105%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After 4 KiB random writes (6 intervals)&lt;/td&gt;
&lt;td&gt;1,151-1,267 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.7-11.8 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;177,527-189,629&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89-95%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;169-188%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DiskRead / DataRead&lt;/code&gt; is about 100%, so it is reading from disk right after the fill too.&lt;/strong&gt;&lt;br&gt;
It is not being served from memory. What changed is the size of one read on the disk side, by a&lt;br&gt;
factor of 7.5. Disk IOPS rises 7.3x as a result, and saturates at 89-95% of the specified value.&lt;/p&gt;

&lt;p&gt;So: 4 KiB random writes break the layout on disk, and the same 1 MiB read becomes many small&lt;br&gt;
reads. Reads do not change the layout, so they have no effect, and neither does time. Rewriting&lt;br&gt;
sequentially restores a contiguous layout. &lt;strong&gt;This is also why only 4 KiB random reads are&lt;br&gt;
unaffected&lt;/strong&gt;: a random 4 KiB is one read on the disk side whatever the layout is.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;DiskIopsUtilization&lt;/code&gt; alone does not get you here.&lt;/strong&gt; It tells you whether something is&lt;br&gt;
saturated; it does not tell you &lt;strong&gt;why&lt;/strong&gt; it saturated (because each read is small).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And &lt;strong&gt;it goes back and forth within a single run.&lt;/strong&gt; I think this is the strongest evidence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What was running&lt;/th&gt;
&lt;th&gt;One write&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;One disk read&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fill, then 3 reads&lt;/td&gt;
&lt;td&gt;1,024 KiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89.1 / 89.6 / 88.3 / 87.4 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The read &lt;strong&gt;after 30 minutes idle&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;87.9 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;4 KiB random writes&lt;/strong&gt;, then read&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.0 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;58.7 to 11.8 to 11.6 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Sequential refill&lt;/strong&gt;, then read&lt;/td&gt;
&lt;td&gt;1,024 KiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88.2 / 87.7 KiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;30 minutes idle does not move it, a 4 KiB write brings it down, a sequential write brings it back.&lt;/strong&gt;&lt;br&gt;
This is neither a difference between deployments nor measurement scatter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note: this &lt;code&gt;d_io&lt;/code&gt; depends on the workload too.&lt;/strong&gt; In 1 MiB sequential read intervals it is 88 or&lt;br&gt;
11.7 KiB; in 4 KiB random read intervals it is 4-5 KiB. &lt;strong&gt;When comparing, match the client-side&lt;br&gt;
size of one operation.&lt;/strong&gt; Without that, the layout difference and the workload difference mix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a benchmarking practice, I think this is the most portable thing here.&lt;br&gt;
If you publish a sequential read figure, &lt;strong&gt;record whether small random writes were interposed&lt;br&gt;
between writing and reading.&lt;/strong&gt; Without them, the value does not move however many times you read or&lt;br&gt;
however long you wait. With them, it changes by nearly 1.8x. As the next section shows, 300 seconds&lt;br&gt;
of sequential writes does not move it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What breaks it is not writes but small random writes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This section was measured later.&lt;/strong&gt; The wording above reads as "writes break it", and that was&lt;br&gt;
too broad. On the same device, I separated the shape of the write from its duration.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;th&gt;Against the reference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reference (not broken)&lt;/td&gt;
&lt;td&gt;2,364.78 MB/s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random writes for &lt;strong&gt;30 seconds&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;2,343.63 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-0.9% (not broken)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random writes for &lt;strong&gt;300 seconds&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;546.02 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-77% (broken)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After a sequential refill&lt;/td&gt;
&lt;td&gt;2,364.18 MB/s&lt;/td&gt;
&lt;td&gt;-0.03% (fully restored)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sequential writes for 300 seconds&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,365.59 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.03% (not broken)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sequential writes do not break it.&lt;/strong&gt; 300 seconds of writing, 0.03%. What breaks it is small
random writes only, not "writes" in general&lt;/li&gt;
&lt;li&gt;30 seconds does not break it, 300 seconds does. The threshold is somewhere in between, and
where is not known&lt;/li&gt;
&lt;li&gt;The unbroken side does not depend on the specified IOPS. 2,364.78 at 100,000, 2,366.23 at
200,000&lt;/li&gt;
&lt;li&gt;For the broken side, proportionality cannot be written. The 546.02 MB/s here (specified
100,000) and 1,272.8 MB/s on another deployment (specified 200,000) give a ratio of 2.33 —
&lt;strong&gt;it moves in the direction of the specified IOPS but not by 2.00, and the two are not from the
same deployment.&lt;/strong&gt; No run varied the specified IOPS while in the broken state, so this is not a
control experiment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In practice, record whether you "read after small random writes", not whether you "read after&lt;br&gt;
writes".&lt;/strong&gt; A workload that writes sequentially and reads sequentially is not subject to this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The conclusion that you cannot size from a single point is unchanged. What changed is the&lt;br&gt;
reason.&lt;/strong&gt; Not "an unexplained 2.64x", but &lt;strong&gt;"even on the same configuration it moves 1.8x&lt;br&gt;
depending on whether small random writes were interposed after the data being read was last&lt;br&gt;
written".&lt;/strong&gt;&lt;br&gt;
Why the 3,401-3,408 MB/s environment came out higher still &lt;strong&gt;remains unexplained.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The warm-side ceiling sits on the client side
&lt;/h3&gt;

&lt;p&gt;In the warm state, none of the&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/so-file-system-metrics.html" rel="noopener noreferrer"&gt;published utilization metrics&lt;/a&gt;&lt;br&gt;
was at its ceiling. &lt;strong&gt;I used two EC2 clients to decide which side it stops on.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What was measured&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rocky 9.7 (concurrent)&lt;/td&gt;
&lt;td&gt;2,040.69 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RHEL 9.7 (concurrent)&lt;/td&gt;
&lt;td&gt;2,163.86 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,204.55 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One EC2 client alone (same state)&lt;/td&gt;
&lt;td&gt;2,265.7 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The total is 1.86x one client. The ceiling sits on the client side.&lt;/strong&gt; The storage side still had&lt;br&gt;
headroom; it was stopping at what one client could produce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What determines the per-client ceiling was not measured.&lt;/strong&gt; It is about 16-17 Gbps per client, short&lt;br&gt;
of the 50 Gbps of a c5n.9xlarge.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before you write "I measured the storage ceiling", try adding an EC2 client.&lt;/strong&gt;&lt;br&gt;
If the total grows, what you were measuring was the EC2 client's ceiling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same shape appeared on a path that is not block. On 2026-09-19, on the NFS path (first&lt;br&gt;
generation 2,048 MBps, SSD IOPS 40,000, two c5n.9xlarge reading 300 GiB that does not overlap),&lt;br&gt;
1,195.27 alone gave &lt;strong&gt;a total of 1,901.48 MB/s, 1.59x.&lt;/strong&gt; Alone, disk throughput utilization was&lt;br&gt;
63% and not saturated; with two clients it reaches 102-103%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There, the single-client side was followed up the next day, and it turned out to be two settings&lt;br&gt;
rather than a resource.&lt;/strong&gt; Transfer size (&lt;code&gt;rsize&lt;/code&gt; 64 KiB to 1 MiB, +21%) and concurrency (8 to 16&lt;br&gt;
threads, +14%) took a single client to 1,646.17 MB/s, at which point the file system side pins at&lt;br&gt;
102.6%&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md" rel="noopener noreferrer"&gt;measurement, Japanese&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It was followed further (2026-09-20), and "the client side" split into parts. On second&lt;br&gt;
generation 6,144 MBps / 200,000 IOPS with 4 KiB random reads, varying only how the working set is&lt;br&gt;
divided, five ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Division&lt;/th&gt;
&lt;th&gt;Against the reference&lt;/th&gt;
&lt;th&gt;Disk side / specified 200,000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 EC2 client / 1 SVM / 1 volume (reference)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;60.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threads doubled only&lt;/td&gt;
&lt;td&gt;+8.9%&lt;/td&gt;
&lt;td&gt;67.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;2 volumes&lt;/strong&gt; (same SVM)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;66.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 SVMs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+26.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;83.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 EC2 clients&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+52.7%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What acts on it is neither thread count nor volume count, but the number of IP addresses the client&lt;br&gt;
talks to and the number of clients.&lt;/strong&gt; With two clients the disk side reaches 96.6% of the specified&lt;br&gt;
value, and only there does the file system side become the limit&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md" rel="noopener noreferrer"&gt;measurement, Japanese&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The same separation was not done for the 16-17 Gbps per client in this section. There is no&lt;br&gt;
block-side measurement varying transfer size, concurrency and the number of peer IP addresses.&lt;br&gt;
&lt;strong&gt;Had I followed it that far instead of stopping at "the client side", the block side might also&lt;br&gt;
have turned out to be settings and division.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Measuring with one client does not determine the kind of ceiling" holds across protocols, so the&lt;br&gt;
procedure in this section is portable.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  At least it is not concurrency
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The candidate "there must not be enough outstanding requests" fell.&lt;/strong&gt; In the unbroken state, I&lt;br&gt;
varied thread count alone by 4x.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threads&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;th&gt;Response time (&lt;strong&gt;measured&lt;/strong&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;2,363.91 MB/s&lt;/td&gt;
&lt;td&gt;216.588 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;td&gt;2,364.28 MB/s&lt;/td&gt;
&lt;td&gt;433.283 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;td&gt;2,365.58 MB/s&lt;/td&gt;
&lt;td&gt;860.258 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Throughput moves by 0.07% and only response time scales precisely. Applying 4x the threads does&lt;br&gt;
not raise the per-client ceiling; &lt;strong&gt;only the time requests spend waiting goes up 4x.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This table is also where the hazard of &lt;code&gt;iorate=max&lt;/code&gt; is most visible in this article.&lt;br&gt;
"2,364 MB/s" is the same on all three rows, &lt;strong&gt;but the response time behind it spans 216 ms to&lt;br&gt;
860 ms.&lt;/strong&gt; Write throughput alone and that difference disappears.&lt;/p&gt;

&lt;p&gt;The file-protocol side was the same. NFS 4 KiB random reads at 512 / 1,024 / 2,048 threads gave&lt;br&gt;
94,949.6 / 97,915.3 / 97,014.5 IOPS (a spread of 2.2%), with response times of&lt;br&gt;
5.391 / 10.456 / 21.108 ms (measured). &lt;strong&gt;That ceiling is not concurrency either.&lt;/strong&gt;&lt;br&gt;
Note that 94,949.6 is from a deployment with half the SSD capacity (4,096 against 8,192 GiB), and is&lt;br&gt;
0.7% from the original measurement of 95,630. Between those two points, capacity was not&lt;br&gt;
acting on it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not cite those two figures as a same-conditions reproduction. Re-measured on 2026-09-20 with&lt;br&gt;
a 600 GiB working set, the reference was 81,313.9, &lt;strong&gt;14% below the figures above.&lt;/strong&gt;&lt;br&gt;
The original measurement did not record its working set, so the conditions cannot be matched.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Match the time of day when comparing configurations
&lt;/h2&gt;

&lt;p&gt;My first comparison measured four configurations in sequence, across time. And as the section&lt;br&gt;
above shows, a sequential read figure moves 1.85x with elapsed time since the write. &lt;strong&gt;I thought I&lt;br&gt;
was changing the policy; I was changing elapsed time as well.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same shape is not confined to policies. I once wrote a 23% difference for multipathing on versus off,&lt;br&gt;
and that too measured the two at different times. &lt;strong&gt;When comparing configuration A against&lt;br&gt;
configuration B, match the elapsed time from write to read, and if you cannot match it, it is better&lt;br&gt;
not to compare.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  On choosing between protocols
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There is no protocol difference between iSCSI and NVMe/TCP at one connection.&lt;/strong&gt; That conclusion&lt;br&gt;
is easy to miss if you compare figures measured on different file systems, so the comparison is kept&lt;br&gt;
below to show why.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;th&gt;iSCSI&lt;/th&gt;
&lt;th&gt;NVMe/TCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Different file systems side by side (misleading)&lt;/td&gt;
&lt;td&gt;1,135.18 MB/s&lt;/td&gt;
&lt;td&gt;591.64 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;As the documented procedure has it&lt;/td&gt;
&lt;td&gt;1,135.18 MB/s&lt;/td&gt;
&lt;td&gt;1,288.87-3,407.60 MB/s (3 deployments)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random reads under that procedure&lt;/td&gt;
&lt;td&gt;44,593 IOPS&lt;/td&gt;
&lt;td&gt;126,964 / 128,154 IOPS (2 deployments)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read on its own, the top row looks like "the ordering inverts at one connection." &lt;strong&gt;But those two&lt;br&gt;
figures were measured on different file systems, and side by side the difference reads as a protocol&lt;br&gt;
difference that isn't there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I re-measured on one file system. A 600 GiB LUN and a 600 GiB namespace were placed together on&lt;br&gt;
a single 1,800 GiB volume, leaving &lt;strong&gt;&lt;code&gt;lun=&lt;/code&gt; as the only line differing between the parameter files.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;One connection, same file system&lt;/th&gt;
&lt;th&gt;1 MiB sequential read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iSCSI, 1 session&lt;/td&gt;
&lt;td&gt;1,135.19 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVMe/TCP, 1 I/O queue&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,135.88 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difference&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.06%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The ordering does not invert. There was no protocol difference at one connection.&lt;/strong&gt;&lt;br&gt;
591.64 belongs to the deployment tier: one-connection figures appear at only two positions,&lt;br&gt;
4.73 Gbps and 9.08 Gbps, and within one deployment iSCSI and NVMe/TCP appear at the same&lt;br&gt;
position. The &lt;code&gt;connect-all&lt;/code&gt; side gave 2,366.32 against the published 2,366.49, agreeing to 0.007%&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md" rel="noopener noreferrer"&gt;measurement, Japanese&lt;/a&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The question left over is what 591.64 was.&lt;/strong&gt; In the single mode as published, all four workloads&lt;br&gt;
landed between 591.5 and 591.7, &lt;strong&gt;the shape of hitting the ceiling of a single path.&lt;/strong&gt; The&lt;br&gt;
re-measured run does not pin there. &lt;strong&gt;The cause has not been confirmed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No ratio is written. The &lt;code&gt;as the documented procedure has it&lt;/code&gt; row still has its numerator and&lt;br&gt;
denominator from different file systems, and the NVMe/TCP side scatters across deployments.&lt;br&gt;
Computed from one pair it comes to "2.08x", but &lt;strong&gt;recomputed over the measured spread it is&lt;br&gt;
1.14-3.00x.&lt;/strong&gt; Pick one figure out of that spread and write "N times", and the picking is itself the&lt;br&gt;
conclusion.&lt;/p&gt;

&lt;p&gt;The iSCSI side cannot be treated as a fixed value either. For sequential reads, one session and&lt;br&gt;
both portals agreed across two deployments (1,135.18 and 1,135.19, 0.01 MB/s apart), but &lt;strong&gt;two&lt;br&gt;
points are not a distribution.&lt;/strong&gt; And 16 sessions is still one deployment only. There is no&lt;br&gt;
basis for "iSCSI is stable". The evidence is simply not there.&lt;/p&gt;

&lt;p&gt;One thing can be kept as a comparison. 4 KiB random reads agreed to within 1% on the same two&lt;br&gt;
deployments where sequential reads scattered (126,964 and 128,154). &lt;strong&gt;It is the sequential read&lt;br&gt;
that scatters, and nothing else.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was later corroborated. On the same deployment where a rewrite moved sequential reads by&lt;br&gt;
1.85x, 4 KiB random reads gave 131,506 (warm) against 131,550 / 131,749 / 131,736 (cold), a spread&lt;br&gt;
of 0.2%. &lt;strong&gt;Whatever moves sequential reads by 1.85x does not act on 4 KiB random reads.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;4 KiB random reads do, however, scatter across deployments for a different reason (three tiers&lt;br&gt;
observed: 131k / 203k / 242k). That cause could not be confirmed. Two things were established.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One is that 4 KiB random reads scale precisely with the specified SSD IOPS.&lt;/strong&gt;&lt;br&gt;
Only the specified value was changed, within one deployment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provisioned SSD IOPS&lt;/th&gt;
&lt;th&gt;4 KiB random read&lt;/th&gt;
&lt;th&gt;Ratio to the specified value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200,000 (before lowering)&lt;/td&gt;
&lt;td&gt;131,413.5 IOPS&lt;/td&gt;
&lt;td&gt;0.657&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;100,000&lt;/strong&gt; (after lowering)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;66,016.9 IOPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.660&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000 (created that way on another deployment)&lt;/td&gt;
&lt;td&gt;65,070-65,947 IOPS&lt;/td&gt;
&lt;td&gt;0.651-0.659&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;200,000&lt;/strong&gt; (after &lt;strong&gt;raising&lt;/strong&gt; it on that deployment)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;131,529.7 IOPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.658&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ratio going down is 1.990 and going up is 2.02. &lt;strong&gt;The measured value lands at about 66% of the&lt;br&gt;
specified value, the same ratio at all four points.&lt;/strong&gt; The 1 MiB sequential read in the same&lt;br&gt;
states was 2,265.7 / 2,366.23 MB/s and does not change: sequential reads do not depend on the&lt;br&gt;
specified IOPS.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The reverse control was taken differently, in the following run.&lt;/strong&gt; The first attempt lowered from&lt;br&gt;
200,000, and the request to restore it was refused with "cannot start until at least 6 hours have&lt;br&gt;
passed since the last storage capacity update".&lt;br&gt;
&lt;strong&gt;A provisioned IOPS change is treated as a storage capacity update, with a six-hour cooldown.&lt;/strong&gt;&lt;br&gt;
Billing continues while you wait, so &lt;strong&gt;the next run was created at 100,000 and raised to 200,000&lt;br&gt;
partway through&lt;/strong&gt;: in the increasing direction it takes one update and does not hit the cooldown.&lt;br&gt;
&lt;strong&gt;An update stays in &lt;code&gt;UPDATED_OPTIMIZING&lt;/code&gt; for about 18 minutes&lt;/strong&gt; (about 17 minutes when lowering).&lt;br&gt;
&lt;strong&gt;If you measure by lowering, plan to throw the environment away; if you need a round trip, design&lt;br&gt;
it in the increasing direction.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The other is that what sets the spread between deployments is the disk read amplification factor.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The specified value was 200,000 on every deployment. What differed was &lt;strong&gt;how many disk-side reads one&lt;br&gt;
client read becomes.&lt;/strong&gt; CloudWatch retains 5-minute values for 63 days, so the intervals whose&lt;br&gt;
average read I/O is 4 KiB can be pulled out and checked without rebuilding the environment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Client IOPS&lt;/th&gt;
&lt;th&gt;Disk IOPS&lt;/th&gt;
&lt;th&gt;Against the specified 200,000&lt;/th&gt;
&lt;th&gt;Amplification (byte ratio)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High side&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;203,961&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;162,562&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;81%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High side (another deployment)&lt;/td&gt;
&lt;td&gt;206,582&lt;/td&gt;
&lt;td&gt;172,158&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low side&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;131,617&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;195,905&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;193.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low side (another deployment)&lt;/td&gt;
&lt;td&gt;130,080&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;195,941&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;190.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The IOPS the client sees is the specified IOPS divided by that environment's amplification factor.&lt;/strong&gt;&lt;br&gt;
131,617 x 1.49 = 196,109; 203,961 x 0.797 = 162,557. Both check out.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At an amplification of about 1.5, &lt;strong&gt;the disk saturates at 98% of the specified value&lt;/strong&gt; and the
client stops at 131k&lt;/li&gt;
&lt;li&gt;At an amplification of about 0.8, &lt;strong&gt;the disk has headroom at 81%&lt;/strong&gt; and the client reaches 204k&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The "about 66% of the specified value" from the proportionality above is the reciprocal of that&lt;br&gt;
1.5.&lt;/strong&gt; The same relation was being seen two different ways.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Here I nearly settled on a wrong explanation once.&lt;/strong&gt; I thought "if it exceeds the specified value&lt;br&gt;
it must be served from memory", but &lt;strong&gt;the way to decide that is the ratio of &lt;code&gt;DiskReadBytes&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;DataReadBytes&lt;/code&gt;&lt;/strong&gt; (a few percent or less means it is served from cache). Applied for real it was&lt;br&gt;
&lt;strong&gt;104-181% across all five deployments: none of them was being served from cache.&lt;/strong&gt; The cause&lt;br&gt;
was looking only at &lt;code&gt;DiskIopsUtilization&lt;/code&gt;. &lt;strong&gt;It tells you whether the disk is saturated; it tells&lt;br&gt;
you nothing about the relation to what the client sees.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you measure, take four&lt;/strong&gt;: &lt;code&gt;DataReadBytes&lt;/code&gt; / &lt;code&gt;DataReadOperations&lt;/code&gt; / &lt;code&gt;DiskReadBytes&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;DiskReadOperations&lt;/code&gt;. Taken in 5-minute intervals, the average I/O size lets you cut out workload&lt;br&gt;
intervals after the fact. &lt;strong&gt;You do not have to rely on the timestamps in your own notes.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;So what is the reason the amplification factor splits between 0.8 and 1.5? This is what remained to&lt;br&gt;
the end.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here another of my hypotheses was disproved. I thought "the layout above must also be setting the&lt;br&gt;
amplification factor", but even right after a sequential refill had restored the layout to 88 KiB,&lt;br&gt;
random read amplification stayed at 190-208%. &lt;strong&gt;The layout for sequential reads and the&lt;br&gt;
amplification factor for random reads are separate variables.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There was one candidate. Line up the five deployments by &lt;strong&gt;"did 4 KiB random writes ever run on that&lt;br&gt;
device"&lt;/strong&gt; and they split cleanly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment and interval&lt;/th&gt;
&lt;th&gt;4 KiB random write history&lt;/th&gt;
&lt;th&gt;Amplification&lt;/th&gt;
&lt;th&gt;Client IOPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Never ran (a run that measured reads only)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.5%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;203,961&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An interval before it had run&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;83.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;206,582&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After it ran&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;170.1%&lt;/td&gt;
&lt;td&gt;140,423&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After it ran&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;193.7%&lt;/td&gt;
&lt;td&gt;131,617&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After it ran (&lt;strong&gt;a sequential refill does not restore it&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;190-208%&lt;/td&gt;
&lt;td&gt;97,769-130,080&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All five followed that ordering. Even so, &lt;strong&gt;this is an ordering of observations, not a control&lt;br&gt;
experiment.&lt;/strong&gt; I wrote it down as something that would be settled by applying the steps in turn on a&lt;br&gt;
single device.&lt;/p&gt;

&lt;h3&gt;
  
  
  And that candidate was disproved too
&lt;/h3&gt;

&lt;p&gt;I applied it. &lt;strong&gt;It was disproved.&lt;/strong&gt; On a new deployment I began measuring right after a fill, with&lt;br&gt;
4 KiB random writes never having run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;4 KiB random read&lt;/th&gt;
&lt;th&gt;Against the specified 100,000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Right after a fill on a new file system&lt;/strong&gt; (no random write history)&lt;/td&gt;
&lt;td&gt;65,947.6 IOPS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.659&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After 300 seconds of 4 KiB random writes&lt;/td&gt;
&lt;td&gt;65,430.8 IOPS&lt;/td&gt;
&lt;td&gt;0.654&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After a sequential refill&lt;/td&gt;
&lt;td&gt;65,070.0 IOPS&lt;/td&gt;
&lt;td&gt;0.651&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is already 0.659 with no history. Where the "none" rows in the table above were 0.83-0.97,&lt;br&gt;
&lt;strong&gt;the same "none" condition produced a value on the 1.5 side.&lt;/strong&gt; The spread across the three states is&lt;br&gt;
1.3%, and the amplification factor moves with neither layout nor write history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five observations lining up cleanly was a coincidence.&lt;/strong&gt; That is the third disproved hypothesis.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An ordering of observations is not a substitute for a control experiment.&lt;/strong&gt; All five followed it&lt;br&gt;
without exception, the mechanism was plausible, and it was wrong. &lt;strong&gt;When you write "all n cases&lt;br&gt;
follow" as a basis, check at the same time how many unvaried variables were lining up in the same&lt;br&gt;
direction.&lt;/strong&gt; In these five, the patch version and the client were lined up the same way too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What remains is those two. The difference between the 0.83-0.97 of F-6 / F-7 and the 1.5 side&lt;br&gt;
later on comes down to the ONTAP patch version (9.18.1P6 against 9.18.1) and the client used&lt;br&gt;
for the measurement. &lt;strong&gt;Neither was varied within a single run, so they are named as candidates and&lt;br&gt;
nothing more.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One of those two cannot be varied by a reader either.&lt;/strong&gt; &lt;code&gt;CreateFileSystem&lt;/code&gt;'s&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/APIReference/API_CreateFileSystem.html" rel="noopener noreferrer"&gt;&lt;code&gt;FileSystemTypeVersion&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
is documented as being for FSx for Lustre, and FSx for ONTAP has no parameter for specifying a&lt;br&gt;
version. The list of updatable properties in&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/updating-file-system.html" rel="noopener noreferrer"&gt;Updating file systems&lt;/a&gt;&lt;br&gt;
does not include the ONTAP version either. &lt;strong&gt;So the patch version can be neither chosen nor&lt;br&gt;
pinned&lt;/strong&gt;, and a control experiment varying that variable cannot be assembled on the user side.&lt;br&gt;
&lt;strong&gt;The client is the only one that can be varied.&lt;/strong&gt; If you want to press on here, what you can do is&lt;br&gt;
show that the amplification factor does not move when the client is changed, narrowing the&lt;br&gt;
candidate to the patch version. &lt;strong&gt;It does not establish causation.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The policy, incidentally, had nothing to do with it. Switching &lt;code&gt;queue-depth&lt;/code&gt; to &lt;code&gt;numa&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;round-robin&lt;/code&gt; to &lt;code&gt;queue-depth&lt;/code&gt; during a 4 KiB random read produced no step, and against the 132,044.8&lt;br&gt;
IOPS average of the run containing all three policies, &lt;code&gt;queue-depth&lt;/code&gt; alone gave 131,413.5 IOPS —&lt;br&gt;
a difference of 0.5%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things I hit on the instrument and the teardown
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It took nine failures to reach a measurement. All of them gates, none of them numbers.&lt;/strong&gt;&lt;br&gt;
Four of those happened before reaching a mount, so they are in a&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/moving-on-premises-block-storage-to-aws-the-divide-by-625-mbps-sizing-approach-comes-out-wrong-31pc"&gt;separate article&lt;/a&gt;&lt;br&gt;
(A: sessions and queues); what belongs here are the five on the instrument and the teardown.&lt;br&gt;
The detail is in the&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/block-measurement-runbook.md" rel="noopener noreferrer"&gt;block measurement runbook&lt;/a&gt;&lt;br&gt;
as a table of symptom, cause and how to check.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;vdbench resolves &lt;code&gt;include=&lt;/code&gt; against the current directory, not against the path given to &lt;code&gt;-f&lt;/code&gt;.
The fill "completed successfully without writing anything". &lt;strong&gt;Had I gone on without noticing, I would
have been measuring reads against unwritten blocks (which return zeros).&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;6. &lt;code&gt;key=value&lt;/code&gt; on a vdbench command line is not an override of the workload definition.&lt;/strong&gt;&lt;br&gt;
It is an assignment to a placeholder inside the parameter file. Passing &lt;code&gt;xfersize=1024k&lt;/code&gt; failed with&lt;br&gt;
&lt;code&gt;Unused parameter substitution&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I reported an SSM wait limit as a failure. A 600 GiB write did not fit inside a 7.5-minute&lt;br&gt;
wait, I saw &lt;code&gt;status: InProgress&lt;/code&gt;, judged it a failure and tore the environment down.&lt;br&gt;
&lt;strong&gt;A wait limit is not a failure.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A sentinel string matched my own explanatory text. Against an implementation that "prints&lt;br&gt;
MISSING if the module is absent", I was deciding with &lt;code&gt;grep -q 'MISSING'&lt;/code&gt;, and it matched the&lt;br&gt;
explanatory line "A MISSING line means…" in the same output, so F-2 was skipped although NVMe/TCP&lt;br&gt;
was available. &lt;strong&gt;A sentinel that your own explanatory text satisfies is not a sentinel.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The teardown trap died partway through the teardown. Under &lt;code&gt;set -euo pipefail&lt;/code&gt;, reading&lt;br&gt;
&lt;code&gt;describe-stacks&lt;/code&gt; after a delete always fails once the stack has finished disappearing, and that&lt;br&gt;
failure kills the trap itself. The delete ran but the retry did not, and a &lt;code&gt;DELETE_FAILED&lt;/code&gt; stack was&lt;br&gt;
left behind. &lt;strong&gt;Put the delete call first in the trap, and &lt;code&gt;set +e&lt;/code&gt; immediately after it.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Three more hit during the rewrite
&lt;/h3&gt;

&lt;p&gt;The nine above are from the first measurement. The run that recounted things per path added three&lt;br&gt;
more. &lt;strong&gt;All of them the same shape as the nine: looking successful while measuring nothing, or&lt;br&gt;
not being able to tell that nothing was measured.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A counter table returned empty. &lt;code&gt;nvmf_lif&lt;/code&gt; had zero rows even after 600 GiB had been written,&lt;br&gt;
and &lt;strong&gt;reading zero rows as zero bytes gets you the right conclusion for the wrong reason.&lt;/strong&gt; Which&lt;br&gt;
table to look at, and the detail of that trap, are in the &lt;a href="https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl"&gt;separate article&lt;/a&gt;&lt;br&gt;
(B).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A socket aggregation matched my own metric. To sum bytes per destination I treated any field&lt;br&gt;
ending in &lt;code&gt;:4420&lt;/code&gt; as a destination, and &lt;code&gt;bytes_acked:4420&lt;/code&gt; (a byte count) happened to end in&lt;br&gt;
4420, growing one spurious path named &lt;code&gt;bytes_acked&lt;/code&gt;. The value was 0 so the sum was not broken, but&lt;br&gt;
&lt;strong&gt;it is the same mistake as the eighth.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;12. "It passed locally so it will pass in CI" did not hold.&lt;/strong&gt; This is the repository side rather&lt;br&gt;
than measurement, but the structure is the same. A check scans tracked files, and a new file is&lt;br&gt;
outside the scan while it is untracked. Green locally, red in CI. "Check after the last edit" is&lt;br&gt;
not enough; it is also "after the last &lt;code&gt;git add&lt;/code&gt;".&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The same configuration varied by 2.64x across environments.&lt;/strong&gt; Do not size from a single measured
value&lt;/li&gt;
&lt;li&gt;What is acting on it is the layout on disk. One disk-side read goes from 87-89 KiB to 11.7 KiB
(a factor of 7.5), disk IOPS rises 7.3x and saturates at 89-95% of the specified value.
&lt;strong&gt;&lt;code&gt;DiskRead / DataRead&lt;/code&gt; is about 100%, so the faster side is reading from disk too.&lt;/strong&gt; It was not
being served from memory&lt;/li&gt;
&lt;li&gt;What breaks it is small random writes only. Three reads in a row, a spread of 0.2%; 30 minutes
idle, within 0.4%; 300 seconds of sequential writes, 0.03%. 4 KiB random writes cost 0.9% at
30 seconds and 77% at 300 seconds, and a sequential refill restores it. It goes back and forth
within a single run. &lt;strong&gt;Where the threshold sits between 30 and 300 seconds is not known&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The warm-side ceiling sits on the client side, and it is not concurrency. Two clients
concurrently total 4,204.55 MB/s, 1.86x one client. Meanwhile 512 to 2,048 threads moves
throughput by only 0.07% and only scales response time from 216.6 to 860.3 ms. NFS 4 KiB random
reads are the same, a spread of 2.2% (5.391 to 21.108 ms). &lt;strong&gt;Before you write "I measured the
storage ceiling", try adding an EC2 client. Adding threads will not tell you&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;4 KiB random reads scale precisely with the specified SSD IOPS (about 66% of the specified
value, the same ratio at four points). It holds in both the decreasing and the increasing
direction. &lt;strong&gt;Sequential reads do not depend on the specified IOPS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;What splits IOPS between 131k and 204k across deployments is the disk read amplification factor.
Client IOPS = specified IOPS / amplification factor. &lt;strong&gt;&lt;code&gt;DiskIopsUtilization&lt;/code&gt; alone will not tell
you.&lt;/strong&gt; Take &lt;code&gt;DataReadBytes&lt;/code&gt; / &lt;code&gt;DataReadOperations&lt;/code&gt; / &lt;code&gt;DiskReadBytes&lt;/code&gt; / &lt;code&gt;DiskReadOperations&lt;/code&gt;.
What sets that amplification factor is not known. That it is neither layout nor write history
is settled; the remaining candidates are the ONTAP patch version and the client. The patch
version cannot be specified on FSx for ONTAP, so a control experiment varying that variable
cannot be assembled on the user side&lt;/li&gt;
&lt;li&gt;Observation order is not a substitute for a control experiment. &lt;strong&gt;I had a candidate that five
cases followed without exception; I aimed a measurement at it and it was disproved.&lt;/strong&gt; Unvaried
variables were simply lined up in the same direction&lt;/li&gt;
&lt;li&gt;When comparing configuration A against B, match the elapsed time from write to read. &lt;strong&gt;I compared
without matching it, wrote "1.73x from the policy", and withdrew it later&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Which protocol comes out ahead changes with the procedure and the I/O shape. Publish which
procedure measured what, not which one is faster&lt;/li&gt;
&lt;li&gt;Do not write throughput alone. The figures in this article are &lt;code&gt;iorate=max&lt;/code&gt; saturation points,
and the same 2,364 MB/s sits at a 216 ms point and at an 860 ms point. &lt;strong&gt;Without response time
alongside it, that difference disappears&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Notes on the AWS documentation
&lt;/h2&gt;

&lt;p&gt;This article contains no remarks about the documentation, and there is nothing to submit.&lt;/p&gt;

&lt;p&gt;What it contains are errors in my own way of measuring and measurements that the published&lt;br&gt;
specification cannot account for. &lt;strong&gt;Neither is a claim that the documentation is wrong, so I am&lt;br&gt;
setting them out separately.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2.64x across the same configuration / 1.85x from layout&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Measurement.&lt;/strong&gt; The published specification does not promise this granularity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The cause of amplification 0.8 against 1.5 is unknown&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Left unresolved.&lt;/strong&gt; The candidates are the ONTAP patch version and the client, and &lt;strong&gt;the former cannot be chosen on FSx for ONTAP&lt;/strong&gt;, so no control experiment can be assembled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The per-client ceiling (block side)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not measured.&lt;/strong&gt; On the NFS side it was followed through on 2026-09-20 (the table above), so the same way of varying things should apply to the block side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparing policies across time, writing 1.73x, and withdrawing it&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;My error&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nvmf_lif&lt;/code&gt; returning no rows&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;ONTAP-side behaviour.&lt;/strong&gt; A question for the vendor, not a matter of AWS documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The remarks about the documentation are in the other two: &lt;a href="https://dev.to/aws-builders/moving-on-premises-block-storage-to-aws-the-divide-by-625-mbps-sizing-approach-comes-out-wrong-31pc"&gt;A&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl"&gt;B&lt;/a&gt;, each with its own submission status.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Submission status across the three block-protocol articles and S3 Burst Part 2 is collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-aws-feedback-status.md" rel="noopener noreferrer"&gt;AWS documentation notes and inquiries across the block-protocol articles&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary record of the figures
&lt;/h2&gt;

&lt;p&gt;The primary record of the figures and their conditions is in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md" rel="noopener noreferrer"&gt;per-protocol measurement results (Japanese)&lt;/a&gt;,&lt;br&gt;
and the reproduction procedure is in the&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/block-measurement-runbook.md" rel="noopener noreferrer"&gt;block measurement runbook (Japanese)&lt;/a&gt;.&lt;br&gt;
&lt;strong&gt;Whatever is unconfirmed is written as unconfirmed, so please do not drop that when citing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pitfalls with the VDBENCH measurement tool, and items to always record, are collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-testing-guide.md" rel="noopener noreferrer"&gt;Considerations when measuring FSx for ONTAP's block protocols&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>performance</category>
      <category>monitoring</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Moving on-premises block storage to AWS? The "divide by 625 MBps" sizing approach comes out wrong</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:43:26 +0000</pubDate>
      <link>https://dev.to/aws-builders/moving-on-premises-block-storage-to-aws-the-divide-by-625-mbps-sizing-approach-comes-out-wrong-31pc</link>
      <guid>https://dev.to/aws-builders/moving-on-premises-block-storage-to-aws-the-divide-by-625-mbps-sizing-approach-comes-out-wrong-31pc</guid>
      <description>&lt;h1&gt;
  
  
  Moving on-premises block storage to AWS? The "divide by 625 MBps" sizing approach comes out wrong — measured session and queue counts
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;What this article does and does not cover: it covers the measured failure of the "divide by&lt;br&gt;
625 MBps" sizing approach, and four configuration failures hit before mounting. It does not&lt;br&gt;
cover performance conclusions, a comparison against file protocols, or distributing block storage&lt;br&gt;
to multiple sites (details under "What this article does not cover" below).&lt;br&gt;
&lt;strong&gt;All figures are from a Single-AZ configuration; Multi-AZ is unverified.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When using FSx for ONTAP over iSCSI or NVMe/TCP, how many sessions should you open? AWS's&lt;br&gt;
procedures surface one number: &lt;strong&gt;"Amazon EC2 single client maximum of 5 Gbps (~625 MBps)"&lt;/strong&gt;,&lt;br&gt;
which appears on both the&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/mount-iscsi-luns-linux.html" rel="noopener noreferrer"&gt;iSCSI&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/provision-nvme-linux.html" rel="noopener noreferrer"&gt;NVMe/TCP&lt;/a&gt; pages as&lt;br&gt;
the lead-in to the procedure for adding sessions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One thing first: AWS does not instruct you to divide.&lt;/strong&gt; The source text says "if you want&lt;br&gt;
throughput beyond this value, follow the EC2 network bandwidth page's procedure to add&lt;br&gt;
sessions," and it's marked &lt;code&gt;(Optional)&lt;/code&gt;. No formula is given. "required bandwidth ÷ 625 =&lt;br&gt;
session count" is a sizing approach I built after reading that figure. This article tests&lt;br&gt;
my sizing approach, not a formula AWS published.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Measuring it, that sizing approach &lt;strong&gt;missed in both directions.&lt;/strong&gt; 1.82x with one session, 0.20x&lt;br&gt;
with sixteen. And going from one session to two moved sequential throughput by zero bytes.&lt;/p&gt;

&lt;p&gt;So this figure can't be used as a divisor to determine session count. What's documented as a&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-network-bandwidth.html" rel="noopener noreferrer"&gt;single flow's ceiling in EC2 network bandwidth&lt;/a&gt;&lt;br&gt;
turned out to be &lt;strong&gt;a separate thing from what a block protocol session measures per session.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NVMe/TCP has nothing equivalent to &lt;code&gt;nr_sessions&lt;/code&gt;; the analogous quantity is queue count. That&lt;br&gt;
&lt;strong&gt;doesn't honor a request either&lt;/strong&gt; — asking for 36 produced 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was measured
&lt;/h2&gt;

&lt;p&gt;Assuming no prior knowledge of FSx for ONTAP, here's where this article's measurement happens.&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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fb6b83cd3d6bc516f12478138cc36bb30a3ae0ec3%2Fdocs%2F_assets%2Fimages%2Fs3burst-block-a-sessions-en%402x.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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fb6b83cd3d6bc516f12478138cc36bb30a3ae0ec3%2Fdocs%2F_assets%2Fimages%2Fs3burst-block-a-sessions-en%402x.png" alt="Inside a single VPC and single AZ, one EC2 client (c5n.9xlarge) connects to one Amazon FSx for&lt;br&gt;
NetApp ONTAP (second-generation SINGLE_AZ_2) file system over either iSCSI (multiple sessions) or&lt;br&gt;
NVMe/TCP (multiple queues)" width="800" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: what this article measures is the connection count between one EC2 client and one FSx for&lt;br&gt;
ONTAP file system, not multiple clients pooled together.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The figure states the same thing as the table below. What this article measures is the&lt;br&gt;
&lt;strong&gt;connection count&lt;/strong&gt; between one EC2 client and one FSx for ONTAP file system, not multiple&lt;br&gt;
clients pooled together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Role in this article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VPC / AZ&lt;/td&gt;
&lt;td&gt;Single AZ, no network detour, so what's measured is the protocol itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 client&lt;/td&gt;
&lt;td&gt;Varies session/queue count and measures 1 MiB sequential reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;Provides the iSCSI LUN or the NVMe/TCP namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection count&lt;/td&gt;
&lt;td&gt;For iSCSI, TCP sessions counted with &lt;code&gt;ss&lt;/code&gt;; for NVMe/TCP, requested vs. effective queue count&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What this article does not cover. No performance conclusion is drawn; the numbers here only&lt;br&gt;
serve as evidence that the division misses. &lt;strong&gt;The same configuration swings 2.64x across&lt;br&gt;
environments&lt;/strong&gt;, so don't use these figures for sizing. Why, and what actually drives the numbers,&lt;br&gt;
is in a &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/.private/blog-unpublished-block-c-what-moves-the-numbers-en.md" rel="noopener noreferrer"&gt;separate article&lt;/a&gt;.&lt;br&gt;
There's no comparison against file protocols (NFS/SMB) either — there's a reason they don't belong&lt;br&gt;
in the same table. And there's no discussion of distributing block storage to multiple sites — this&lt;br&gt;
architecture's distribution layer (FlexCache) distributes volumes, not LUNs or namespaces.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is one of a set of three, but there's no order to read them in.&lt;/strong&gt; The block protocol&lt;br&gt;
measurements are split by reader.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Session and queue counts&lt;/strong&gt; (this article)&lt;/td&gt;
&lt;td&gt;Anyone about to mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl"&gt;Checking whether ANA is available&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Anyone who followed the procedure and got stuck&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi"&gt;What moves the numbers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Anyone measuring or citing figures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None assumes the other two. Read only what you need.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Measurement environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File system&lt;/td&gt;
&lt;td&gt;FSx for ONTAP second-generation SINGLE_AZ_2, 6,144 MBps x1 HA pair, SSD 4,096 GiB, provisioned 200,000 IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1, single AZ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;9.18.1P5&lt;/strong&gt; (the three divisor-table data points). &lt;strong&gt;The queue negotiation section used a separate deployment, and its version was not recorded&lt;/strong&gt; — it wasn't read at measurement time and can't be recovered after the fact. &lt;strong&gt;I can't write "all figures in this article are the same version."&lt;/strong&gt; (Multiple versions coexisting across this configuration is covered in the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/.private/blog-unpublished-block-c-what-moves-the-numbers-en.md" rel="noopener noreferrer"&gt;separate article&lt;/a&gt; (C). The cross-article version mapping is in the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-ontap-version-matrix.md" rel="noopener noreferrer"&gt;ONTAP version matrix&lt;/a&gt;.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVMe read cache&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Disabled&lt;/strong&gt; (confirmed on both nodes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LUN / namespace&lt;/td&gt;
&lt;td&gt;600 GiB, on a 900 GiB volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before measuring&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Wrote the full 600 GiB once.&lt;/strong&gt; Thin-provisioned unwritten blocks return zero, so reading without writing first measures nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;c5n.9xlarge; 50 Gbps network is the guaranteed value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measurement tool&lt;/td&gt;
&lt;td&gt;VDBENCH 5.04.07 direct, &lt;strong&gt;&lt;code&gt;iorate=max&lt;/code&gt; (unlimited)&lt;/strong&gt;, 512 threads, 60s warmup + 300s measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test data / efficiency settings&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not recorded whether the VDBENCH payload was compressible, or whether volume inline efficiency (&lt;code&gt;StorageEfficiencyEnabled&lt;/code&gt;) was enabled.&lt;/strong&gt; On the file-protocol side, efficiency settings measurably affected figures; whether the same applies to block is unconfirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Because the window is 300 seconds, this article's figures include bursts. They can't be cited as&lt;br&gt;
a baseline.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This article's figures are the saturation point of &lt;code&gt;iorate=max&lt;/code&gt;, not an operating point in&lt;br&gt;
production. No target IOPS was given — this is however much could be pushed through. On the&lt;br&gt;
file-protocol side of the same repository, giving a target of 4,400 IOPS produced 4,406 MB/s at&lt;br&gt;
8.86 ms, while unlimited produced 4,204 MB/s at 121.79 ms. &lt;strong&gt;Unlimited is 5% lower&lt;br&gt;
throughput and 14x the response time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With threads fixed at 512 and unlimited, response time is an identity of &lt;code&gt;512 / IOPS&lt;/code&gt; (confirmed&lt;br&gt;
against a separate measurement: 216.6 ms derived versus 216.588 ms measured). The values below&lt;br&gt;
can be re-read through that identity — 1,135.18 MB/s is about 451 ms, 1,970.43 MB/s is&lt;br&gt;
about 260 ms. &lt;strong&gt;Both are derived values, not recorded measurements.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The divisor division doesn't hold
&lt;/h2&gt;

&lt;p&gt;Over iSCSI, I varied the &lt;strong&gt;counted number of TCP connections&lt;/strong&gt; and measured 1 MiB sequential&lt;br&gt;
reads. These are actual counts from &lt;code&gt;ss&lt;/code&gt;, not the requested value.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TCP counted&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;th&gt;Predicted from 625 MBps&lt;/th&gt;
&lt;th&gt;Measured / predicted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1,135.18 MB/s&lt;/td&gt;
&lt;td&gt;625 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.82x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1,135.18 MB/s&lt;/td&gt;
&lt;td&gt;1,250 MB/s&lt;/td&gt;
&lt;td&gt;0.91x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;1,970.43 MB/s&lt;/td&gt;
&lt;td&gt;10,000 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.20x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With one session it's faster than predicted; with sixteen it's a fifth of the prediction.&lt;/p&gt;

&lt;p&gt;And going from one session to two moves sequential throughput by zero bytes — 1,135.18 appears&lt;br&gt;
twice in a row. &lt;strong&gt;So the ceiling hit with one session is not a single flow's capacity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;1,135 MB/s is 9.08 Gbps. In the same environment, the file protocols landed near the documented&lt;br&gt;
5 Gbps on a single connection — NFS 591.62 MB/s (4.73 Gbps), SMB 574.24 MB/s (4.59 Gbps).&lt;br&gt;
&lt;strong&gt;A single iSCSI session carries roughly twice that. Why is unconfirmed.&lt;/strong&gt; (This comparison is&lt;br&gt;
against single-connection figures from other protocols in the same environment, not a direct A/B&lt;br&gt;
test against iSCSI.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's the split between what's documented and what's an unverified candidate. &lt;strong&gt;The single-flow&lt;br&gt;
5 Gbps ceiling itself is documented.&lt;/strong&gt; AWS defines a single flow as "a TCP or UDP flow identified&lt;br&gt;
by a unique 5-tuple (source IP, destination IP, source port, destination port, protocol)" and&lt;br&gt;
states that &lt;strong&gt;outside a cluster placement group, a single flow is limited to 5 Gbps&lt;/strong&gt;&lt;br&gt;
(&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-network-bandwidth.html" rel="noopener noreferrer"&gt;Amazon EC2 instance network bandwidth&lt;/a&gt;).&lt;br&gt;
The same page documents the workarounds too — &lt;strong&gt;up to 10 Gbps inside a cluster placement group,&lt;br&gt;
up to 25 Gbps within the same AZ with ENA Express, or spreading traffic over multiple paths with&lt;br&gt;
Multipath TCP (MPTCP)&lt;/strong&gt;&lt;br&gt;
(&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ena-express.html" rel="noopener noreferrer"&gt;ENA Express&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From here on, these are unverified candidates.&lt;/strong&gt; Whether this measurement environment was&lt;br&gt;
inside a cluster placement group was not recorded and is unconfirmed. 9.08 Gbps would fit inside&lt;br&gt;
a 10 Gbps allowance, but &lt;strong&gt;whether this configuration was inside that allowance is unknown.&lt;/strong&gt;&lt;br&gt;
Whether iSCSI, NVMe/TCP, NFS, and SMB all count as "one" the same way against the 5-tuple&lt;br&gt;
definition is also unconfirmed. ENA elastic network interfaces are documented as having up to 8&lt;br&gt;
RSS queues&lt;br&gt;
(&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking-os.html" rel="noopener noreferrer"&gt;Optimize network performance on EC2 Windows instances&lt;/a&gt; —&lt;br&gt;
that page covers Windows; I haven't checked the equivalent for Linux), and if protocols&lt;br&gt;
distribute across CPU cores differently, the apparent ceiling could differ as a result. &lt;strong&gt;This&lt;br&gt;
article does not test these candidates. A separate measurement would be needed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The requested queue count doesn't hold
&lt;/h2&gt;

&lt;p&gt;NVMe/TCP has nothing equivalent to &lt;code&gt;nr_sessions&lt;/code&gt;. &lt;strong&gt;It opens a TCP connection per queue&lt;/strong&gt;, so the&lt;br&gt;
quantity that corresponds to session count is queue count.&lt;/p&gt;

&lt;p&gt;Requesting via &lt;code&gt;--nr-io-queues&lt;/code&gt; produced this. NCQA (Number of Completion Queues Allocated) and&lt;br&gt;
NSQA (Number of Submission Queues Allocated) are the actual completion and submission queue&lt;br&gt;
counts the NVMe Get Feature command reports back to the host.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Requested queue count&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Effective NCQA / NSQA&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Controller count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single flow&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS's procedure as written (one &lt;code&gt;connect-all&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;unspecified&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit queue count&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;36&lt;/strong&gt; (vCPU count)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 (unchanged)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Requesting 36 produced 4 regardless.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't speculation — it's documented on the ONTAP side. &lt;code&gt;vserver nvme subsystem show&lt;/code&gt;'s&lt;br&gt;
description of &lt;code&gt;-default-io-queue-count&lt;/code&gt; states "The actual value used when a connection is&lt;br&gt;
established may vary depending on the host and transport protocol used," and&lt;br&gt;
&lt;a href="https://docs.netapp.com/us-en/ontap-cli/vserver-nvme-show-host-priority.html" rel="noopener noreferrer"&gt;&lt;code&gt;vserver nvme show-host-priority&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
states that &lt;strong&gt;I/O queue count is determined per combination of node, transport, and host&lt;br&gt;
priority.&lt;/strong&gt; NetApp's KB has an example where setting 15 on a subsystem produced 2 on the host&lt;br&gt;
side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't use the example's value as your own environment's value. In the example above,&lt;br&gt;
&lt;code&gt;nvme-tcp&lt;/code&gt; / &lt;code&gt;regular&lt;/code&gt; is 2, but &lt;strong&gt;in this environment it was 4.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a byproduct, one thing left as "unconfirmed" in planning got resolved. &lt;strong&gt;&lt;code&gt;queue_count&lt;/code&gt; includes&lt;br&gt;
the admin queue and matches &lt;code&gt;NCQA + 1&lt;/code&gt;&lt;/strong&gt; (1 -&amp;gt; 2, 4 -&amp;gt; 5). With one queue, there was also exactly&lt;br&gt;
one established TCP connection carrying I/O.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four failures hit before mounting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nine failures were hit before reaching a measurement. All were gates, not numbers.&lt;/strong&gt; Each one&lt;br&gt;
looks like it succeeded while measuring nothing. Four of those happened before mounting, so&lt;br&gt;
they're here; the rest concern the measurement tool and teardown and are in the&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/.private/blog-unpublished-block-c-what-moves-the-numbers-en.md" rel="noopener noreferrer"&gt;separate article&lt;/a&gt; (C:&lt;br&gt;
what moves the numbers). Details, as a table of symptom / cause / how to check, are in the&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/block-measurement-runbook.md" rel="noopener noreferrer"&gt;block measurement runbook&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;JunctionPath&lt;/code&gt; turned out to be required. I had assumed it wasn't needed since a LUN isn't
read through a junction, but the API rejects that. Worse, the
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-fsx-volume-ontapconfiguration.html" rel="noopener noreferrer"&gt;property reference&lt;/a&gt;'s
entry for &lt;code&gt;JunctionPath&lt;/code&gt; says &lt;code&gt;This parameter is required.&lt;/code&gt; in the body while the same property's
&lt;code&gt;Required:&lt;/code&gt; column says &lt;code&gt;No&lt;/code&gt;. &lt;strong&gt;That's a literal self-contradiction&lt;/strong&gt; (still present as of
2026-09-17). Trust the API. It fails 25 minutes after file system creation starts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same page has an example of writing this without contradiction. &lt;code&gt;StorageEfficiencyEnabled&lt;/code&gt;&lt;br&gt;
uses &lt;code&gt;Required: Conditional&lt;/code&gt; and explains the condition in the body (required when the volume is&lt;br&gt;
&lt;code&gt;RW&lt;/code&gt;). &lt;strong&gt;&lt;code&gt;Conditional&lt;/code&gt; is a value this reference already uses, so &lt;code&gt;JunctionPath&lt;/code&gt; could take the&lt;br&gt;
same form.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can't state exactly which condition makes it required. What I measured is one case,&lt;br&gt;
&lt;code&gt;OntapVolumeType: RW&lt;/code&gt;, where it was required. I did not test whether it's optional for&lt;br&gt;
&lt;code&gt;DP&lt;/code&gt;. Determining the condition belongs to whoever holds the API's implementation;&lt;br&gt;
&lt;strong&gt;what I can report stops at "the body and the &lt;code&gt;Required:&lt;/code&gt; column disagree."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. AL2023 does not create &lt;code&gt;/etc/nvme/hostnqn&lt;/code&gt;.&lt;/strong&gt; AWS's procedure assumes this file exists and&lt;br&gt;
reads it with &lt;code&gt;cat&lt;/code&gt; (RHEL creates it). Generating it with &lt;code&gt;nvme gen-hostnqn&lt;/code&gt; was necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. iSCSI login is asynchronous.&lt;/strong&gt; &lt;code&gt;iscsiadm --login&lt;/code&gt; returns before iscsid actually connects. In&lt;br&gt;
this environment it took about 100 seconds. Counting with &lt;code&gt;ss&lt;/code&gt; right after returned 0, so&lt;br&gt;
every alias and fill attempt right after also came up empty. "The command returned" is not&lt;br&gt;
"the connection is up."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;find_multipaths yes&lt;/code&gt; does not create a map for a single-path device.&lt;/strong&gt; When intentionally&lt;br&gt;
measuring a single flow, the path count is 1 by design, and with the default setting&lt;br&gt;
&lt;code&gt;/dev/mapper/&amp;lt;alias&amp;gt;&lt;/code&gt; never appears. Explicit registration via &lt;code&gt;multipath -a &amp;lt;wwid&amp;gt;&lt;/code&gt; was&lt;br&gt;
necessary. That wwid is also more reliably read from the device with &lt;code&gt;scsi_id&lt;/code&gt; than assembled as&lt;br&gt;
&lt;code&gt;3600a0980&lt;/code&gt;+serial (assembling it is useful as a cross-check, not as the primary source).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sizing session count by dividing required bandwidth by 625 MBps missed in both directions.&lt;/strong&gt;
1.82x with one session, 0.20x with sixteen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Going from one session to two doesn't move sequential throughput.&lt;/strong&gt; The ceiling hit with one
session is not a single flow's capacity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NVMe/TCP queue count is the result of negotiation.&lt;/strong&gt; Requesting 36 produced 4. Read the
effective value with &lt;code&gt;nvme get-feature --feature-id 7&lt;/code&gt;. &lt;code&gt;queue_count&lt;/code&gt; includes the admin queue,
so it equals &lt;code&gt;NCQA + 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All four configuration failures looked like success while measuring nothing.&lt;/strong&gt; &lt;code&gt;JunctionPath&lt;/code&gt;
is required, AL2023 doesn't create &lt;code&gt;hostnqn&lt;/code&gt;, iSCSI login is asynchronous at about 100 seconds,
and &lt;code&gt;find_multipaths yes&lt;/code&gt; doesn't map a single-path device.&lt;/li&gt;
&lt;li&gt;Don't use this article's figures for sizing. The same configuration swings 2.64x across
environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What drives that 2.64x swing is out of scope for this article.&lt;/strong&gt; It's covered in a
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/.private/blog-unpublished-block-c-what-moves-the-numbers-en.md" rel="noopener noreferrer"&gt;separate article&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AWS documentation notes and support inquiries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This article touches two points in the documentation. One has been filed; the other is not&lt;br&gt;
being filed.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;JunctionPath&lt;/code&gt;'s body and &lt;code&gt;Required:&lt;/code&gt; column disagree. Traced to the &lt;a href="https://docs.aws.amazon.com/fsx/latest/APIReference/API_CreateOntapVolumeConfiguration.html" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;, which propagates into &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-fsx-volume-ontapconfiguration.html" rel="noopener noreferrer"&gt;CloudFormation&lt;/a&gt; and the CDK&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Filed (2026-09-17). AWS's reply (2026-09-22): explained that when OntapVolumeType is RW, JunctionPath is required; when DP, it cannot be specified at all — so "Required: No" has a rationale when read across both volume types combined.&lt;/strong&gt; The proposed change to &lt;code&gt;Required: Conditional&lt;/code&gt; was described as feedback to be shared with the internal team, with no commitment on adoption. The question of whether the API reference is upstream of CFN/CDK went unanswered ("internal AWS information, cannot disclose"); instead AWS said it would file feedback against the API reference, CFN, and CDK pages individually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sizing by dividing 5 Gbps (~625 MBps) doesn't match measurement&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/mount-iscsi-luns-linux.html" rel="noopener noreferrer"&gt;iSCSI&lt;/a&gt; / &lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/provision-nvme-linux.html" rel="noopener noreferrer"&gt;NVMe/TCP&lt;/a&gt; procedures&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not filed.&lt;/strong&gt; AWS never states a formula, so this isn't a documentation error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why the second one isn't filed.&lt;/strong&gt; This is a record of my own sizing approach missing, not a&lt;br&gt;
finding about the documentation. The figure itself is correctly documented as a single flow's&lt;br&gt;
ceiling; I was the one who used it as a divisor for session count, and that's where it broke&lt;br&gt;
down.&lt;/p&gt;

&lt;p&gt;The first was filed with AWS Support on 2026-09-17. The submission included the observed&lt;br&gt;
requirement (omitting it fails stack creation 25 minutes in) and the fact that the CDK inherits&lt;br&gt;
it as an optional type, requesting a fix on the API reference side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS's 2026-09-22 reply settles only the rationale behind "Required: No"&lt;/strong&gt; (it holds when RW and&lt;br&gt;
DP are read as one combined attribute). It doesn't change the underlying fact that JunctionPath is&lt;br&gt;
required when creating an RW volume, and whether &lt;code&gt;Required: Conditional&lt;/code&gt; will actually be adopted&lt;br&gt;
remains unconfirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary record of the figures
&lt;/h2&gt;

&lt;p&gt;The primary record of the figures and conditions is in the&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md" rel="noopener noreferrer"&gt;per-protocol measurement results&lt;/a&gt;&lt;br&gt;
(Japanese); the reproduction steps are in the&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/block-measurement-runbook.md" rel="noopener noreferrer"&gt;block measurement runbook&lt;/a&gt;&lt;br&gt;
(Japanese). Anything marked unconfirmed there is marked unconfirmed for a reason — &lt;strong&gt;don't drop&lt;br&gt;
that qualifier when citing it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Considerations before sizing session and queue counts, and pitfalls hit before mounting, are&lt;br&gt;
collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-testing-guide.md" rel="noopener noreferrer"&gt;Considerations when measuring FSx for ONTAP's block protocols&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Submission status across the three block-protocol articles and S3 Burst Part 2 is collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-aws-feedback-status.md" rel="noopener noreferrer"&gt;AWS documentation notes and inquiries across the block-protocol articles&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>performance</category>
      <category>cloud</category>
      <category>linux</category>
    </item>
    <item>
      <title>Amazon S3 Files vs. Amazon FSx for NetApp ONTAP: two designs for using S3 as a file (S3 Burst Part 2)</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:43:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-s3-files-vs-amazon-fsx-for-netapp-ontap-two-designs-for-using-s3-as-a-file-s3-burst-part-4p9m</link>
      <guid>https://dev.to/aws-builders/amazon-s3-files-vs-amazon-fsx-for-netapp-ontap-two-designs-for-using-s3-as-a-file-s3-burst-part-4p9m</guid>
      <description>&lt;h1&gt;
  
  
  Amazon S3 Files vs. Amazon FSx for NetApp ONTAP: two designs for using S3 as a file, measured on the same host (S3 Burst Part 2)
&lt;/h1&gt;

&lt;p&gt;Keeping the primary copy of your data in one place while reaching it both as a file (NFS/SMB)&lt;br&gt;
and over the S3 API: have you had to weigh which design to pick before committing to one?&lt;/p&gt;

&lt;p&gt;There is more than one way on AWS to combine the two. Worth pausing on this, though: every&lt;br&gt;
design has a point where "fast enough" turns into a ceiling, and that point sits in a different&lt;br&gt;
place for each one. It helps to know where each one bottlenecks before choosing. This article&lt;br&gt;
measures three of them from the same host.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl38s3pd4b9p888q2ykcx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl38s3pd4b9p888q2ykcx.png" alt="The three-path configuration and where each bottlenecks" width="799" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same configuration as the image above, reproduced so it reads in environments where images&lt;br&gt;
don't render.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph AWS["AWS Cloud"]
        subgraph VPC["VPC, single AZ"]
            EC2["EC2 client"]
            subgraph A["A: This architecture"]
                AP["S3 Access Point"]
                FSXN[("Amazon FSx for&amp;lt;br/&amp;gt;NetApp ONTAP")]
                AP --&amp;gt; FSXN
            end
            S3["Amazon S3&amp;lt;br/&amp;gt;(bucket)"]
            S3FILES["Amazon S3 Files&amp;lt;br/&amp;gt;(via efs-proxy)"]
            EC2 --&amp;gt;|"write over S3 API"| AP
            FSXN --&amp;gt;|"read over NFS / SMB"| EC2
            EC2 &amp;lt;--&amp;gt;|"read/write over S3 API"| S3
            EC2 &amp;lt;--&amp;gt;|"read/write over NFS"| S3FILES
        end
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The figure states the same thing as the table below. Only A uses a different protocol for&lt;br&gt;
writes versus reads; B and C have the same client reading and writing over the same protocol.&lt;br&gt;
The figure's three panels map directly to the three below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Write&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FSx for ONTAP's S3 Access Point (this architecture, below)&lt;/td&gt;
&lt;td&gt;S3 API&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NFS / SMB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Amazon S3&lt;/td&gt;
&lt;td&gt;S3 API&lt;/td&gt;
&lt;td&gt;S3 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Amazon S3 Files (an S3 bucket mounted over NFS)&lt;/td&gt;
&lt;td&gt;NFS&lt;/td&gt;
&lt;td&gt;NFS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Only A uses a different protocol and client for writes versus reads.&lt;/strong&gt; That's the substance of&lt;br&gt;
this architecture: something written over the S3 API is read over NFS/SMB with no copy job in&lt;br&gt;
between. That's why the figure shows two arrow types for A, while B and C are a single&lt;br&gt;
bidirectional arrow — the same client reads and writes over the same protocol in B and C.&lt;/p&gt;

&lt;p&gt;Measure throughput and lay it out in a table, and the three numbers sit side by side. Sitting side&lt;br&gt;
by side reads as "this one is faster," but each of the three was bottlenecked for a different&lt;br&gt;
reason. Under each panel of the figure is where that path was actually bottlenecked. A throughput&lt;br&gt;
value provisioned on the file system, client-side network bandwidth, the CPU of a process running&lt;br&gt;
on the client. &lt;strong&gt;What the bottleneck is matters more to design than what the ceiling number is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The measuring side has its own prerequisite. &lt;strong&gt;There are five settings that, left at default,&lt;br&gt;
measure something other than what you intended.&lt;/strong&gt; That's covered in the&lt;br&gt;
&lt;a href="https://hakobiya.hatenablog.com/entry/fsxn-file-portal-5-what-only-the-cluster-tells-you" rel="noopener noreferrer"&gt;previous article (Part 5, "five defaults that can invalidate a measurement")&lt;/a&gt;.&lt;br&gt;
This article's figures were measured after changing all five from default.&lt;/p&gt;
&lt;h2&gt;
  
  
  Scope of this article
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What it covers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The three ceilings that bind on the FSx for ONTAP side, and which one actually applied&lt;/li&gt;
&lt;li&gt;What bottlenecks each of the three paths&lt;/li&gt;
&lt;li&gt;What FlexCache does for reads&lt;/li&gt;
&lt;li&gt;What becomes the ceiling as client count scales to 8&lt;/li&gt;
&lt;li&gt;Whether a 15-minute sustained write decays&lt;/li&gt;
&lt;li&gt;The bottleneck shift with small objects&lt;/li&gt;
&lt;li&gt;What each tool can and can't observe&lt;/li&gt;
&lt;li&gt;What hasn't been measured yet&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What it does not cover
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which configuration is faster.&lt;/strong&gt; The three figures in this article &lt;strong&gt;mix a provisioned
ceiling with an unprovisioned elastic ceiling.&lt;/strong&gt; They aren't laid out to be read side by side for
a winner, so wherever a number appears, the type of ceiling it's hitting is stated alongside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to choose a configuration itself.&lt;/strong&gt; Where to place the read side, which side holds the
primary copy, when you want writes visible on the bucket side — these are decided by other
factors and throughput numbers alone don't decide them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The semantics of Amazon S3 Files propagation.&lt;/strong&gt; That file-to-bucket propagation converges
within roughly a 60-second window, and the reverse direction is on the order of seconds, is a
different subject and belongs in a separate article.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM policy and the authorization model.&lt;/strong&gt; The AWS side of an S3 Access Point (IAM identity
policy, access point policy, VPC endpoint policy, SCP) and ONTAP's two-layer authorization are a
different axis from this article's subject (throughput). Minimum-privilege policy examples are
in the
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/limits/s3ap-design-guide.md#authorization-design--least-privilege-policy-examples" rel="noopener noreferrer"&gt;S3 AP design guide's authorization design section&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Measurement environment
&lt;/h2&gt;

&lt;p&gt;Figures can't be reproduced without the environment they came from, so it's placed first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Measured on&lt;/td&gt;
&lt;td&gt;2026-09-01 and 2026-09-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP&lt;/td&gt;
&lt;td&gt;9.18.1P3D1 (the second-generation NFS 2-host measurement used 9.18.1P6. The cross-article version mapping is in the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-ontap-version-matrix.md" rel="noopener noreferrer"&gt;ONTAP version matrix&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File system&lt;/td&gt;
&lt;td&gt;SINGLE_AZ_1 (first-generation Single-AZ). Measured at two throughput capacities, 128 MBps and 2048 MBps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSD&lt;/td&gt;
&lt;td&gt;1024 GiB, &lt;code&gt;DiskIopsConfiguration&lt;/code&gt; at two points, 3,072 and 40,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;c5n.9xlarge (single-client measurements), c5n.2xlarge x8 (client-count scaling test — adding one client at a time and observing how the combined throughput grows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume&lt;/td&gt;
&lt;td&gt;UNIX security style, NFSv3, &lt;code&gt;actimeo=0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test data&lt;/td&gt;
&lt;td&gt;Uncompressible (from &lt;code&gt;/dev/urandom&lt;/code&gt;). Zero-filled data was not used. &lt;strong&gt;These are figures from a world with efficiency features off&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AZ configuration&lt;/td&gt;
&lt;td&gt;All figures are from a Single-AZ configuration. &lt;strong&gt;Multi-AZ is unverified&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both NFS and SMB were measured, but the scope differs. The per-protocol comparison table and&lt;br&gt;
per-path bottlenecks are measured over NFS. What was measured over SMB is the growth pattern with&lt;br&gt;
increasing client count, the 15-minute sustained write, and Multichannel channel count. &lt;strong&gt;The&lt;br&gt;
seven scenarios under cache control were not measured over SMB.&lt;/strong&gt; This architecture can be read&lt;br&gt;
over either NFS or SMB, but this article's figures are all NFS. If you plan to read over SMB,&lt;br&gt;
the figures below don't directly apply. What carries over is the measurement approach and the&lt;br&gt;
procedure for isolating which type of ceiling is binding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproducibility under identical conditions was 0.02-0.14%.&lt;/strong&gt; With that baseline established, a&lt;br&gt;
"moved 30%" statement later in this article can be said not to be run-to-run noise.&lt;/p&gt;

&lt;p&gt;Unless otherwise noted, figures in the body are single measurements. Where something was&lt;br&gt;
measured twice, both values are shown as &lt;code&gt;415.1 / 415.7&lt;/code&gt; rather than averaged or taking a median.&lt;br&gt;
Totals in the client-count tables are the sum of each host's value.&lt;/p&gt;

&lt;p&gt;And every figure in this article is a saturation point — however much could be pushed through at&lt;br&gt;
a fixed concurrency. No point gives a target throughput or target IOPS. This distinction&lt;br&gt;
matters as much as stating which type of ceiling applies — a saturation point answers "where does&lt;br&gt;
it choke," not "what response time does it deliver at that load." Measuring both on a separate file&lt;br&gt;
system (second-generation, 6,144 MBps), a target of 4,400 IOPS produced 4,406 MB/s at 8.86 ms,&lt;br&gt;
while unlimited produced 4,204 MB/s at 121.79 ms. &lt;strong&gt;Unlimited is 5% lower throughput and 14x&lt;br&gt;
the response time.&lt;/strong&gt; Source:&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md#fsx-for-ontap-%E7%AC%AC%E4%BA%8C%E4%B8%96%E4%BB%A3nconnect16auto_vdbench%E7%9B%AE%E6%A8%99%E5%80%A4%E3%82%92%E6%8C%AF%E3%81%A3%E3%81%9F%E7%B3%BB%E5%88%97" rel="noopener noreferrer"&gt;the series with a varied target value&lt;/a&gt;&lt;br&gt;
(Japanese).&lt;/p&gt;

&lt;p&gt;Response time exists for the S3 API path and not for the file path. The S3 API side's body&lt;br&gt;
notes p50 alongside throughput (363.7 ms at concurrency 1, 1054.9 ms at concurrency 64, and so on).&lt;br&gt;
The file path's load generator didn't retain the values it output, and the environment has since&lt;br&gt;
been torn down. &lt;strong&gt;Because concurrency was fixed, it can be re-derived through the identity&lt;br&gt;
&lt;code&gt;concurrency / IOPS&lt;/code&gt;, but that's a derived value, not a measurement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A separate article pushes further on the same property on the block protocol side&lt;br&gt;
(&lt;a href="https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi"&gt;separate article&lt;/a&gt;). It&lt;br&gt;
covers a case where quadrupling thread count moves throughput by only 0.07% while response time&lt;br&gt;
alone quadruples, and &lt;strong&gt;the same 2,364 MB/s appears at both a 216 ms point and an 860 ms point.&lt;/strong&gt;&lt;br&gt;
That same caution applies when citing this article's figures.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The block protocol side is a set of three.&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/moving-on-premises-block-storage-to-aws-the-divide-by-625-mbps-sizing-approach-comes-out-wrong-31pc"&gt;Session and queue counts&lt;/a&gt;&lt;br&gt;
(for anyone about to mount),&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl"&gt;checking whether ANA is available&lt;/a&gt;&lt;br&gt;
(for anyone who followed the procedure and got stuck), and&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi"&gt;what moves the numbers&lt;/a&gt;&lt;br&gt;
(for anyone measuring or citing figures). There's no order to read them in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The measurement host was c5n.9xlarge. The reason is isolation. &lt;strong&gt;With an "Up to"-labeled instance,&lt;br&gt;
once a measured value plateaus, you can't tell whether you hit the storage-side ceiling or the&lt;br&gt;
client-side bandwidth ceiling.&lt;/strong&gt; c5n.9xlarge's 50 Gbps is a guaranteed value. Every figure below&lt;br&gt;
sits well under 50 Gbps, so the possibility that the client-side bandwidth was the cause of a&lt;br&gt;
plateau can be ruled out.&lt;/p&gt;

&lt;p&gt;Only the client-count scaling test used eight c5n.2xlarge instances instead, since that test wants&lt;br&gt;
more clients rather than a bigger single client.&lt;/p&gt;
&lt;h3&gt;
  
  
  The client-count scaling test and the sustained write used a separate file system
&lt;/h3&gt;

&lt;p&gt;The table above is first-generation. Only the client-count scaling test and the 15-minute&lt;br&gt;
sustained write were measured on a separate, second-generation file system. &lt;strong&gt;The first-generation&lt;br&gt;
one is a shared environment, and raising or lowering its provisioned throughput capacity would&lt;br&gt;
affect other measurements.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Measured on&lt;/td&gt;
&lt;td&gt;2026-09-05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File system&lt;/td&gt;
&lt;td&gt;SINGLE_AZ_2 (second-generation Single-AZ), 6,144 MBps x1 HA pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSD&lt;/td&gt;
&lt;td&gt;4,096 GiB, provisioned 200,000 IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;c5n.2xlarge at 1 / 2 / 4 / 6 / 8 hosts. &lt;strong&gt;64 threads and &lt;code&gt;nconnect=16&lt;/code&gt; fixed per host&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because the environment differs, these figures can't be placed in the same column as the table&lt;br&gt;
above. &lt;strong&gt;Load per host was fixed and only the host count was varied, because fixing total thread&lt;br&gt;
count and dividing by host count would change two things at once per data point, making it&lt;br&gt;
impossible to separate which one caused the effect.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Six settings to change from default before measuring throughput (Part 5)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Each one, left at default, measures something other than what you intended.&lt;/strong&gt; And the number&lt;br&gt;
that comes back doesn't look unnatural, so measuring alone won't reveal it. The table and how each&lt;br&gt;
was noticed are in Part 5; only the names are listed here.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Linux's NFS mount opens only one TCP connection by default (plateaus around 590 MB/s)&lt;/li&gt;
&lt;li&gt;Data created with &lt;code&gt;dd if=/dev/zero&lt;/code&gt; (all-zero blocks never reach disk)&lt;/li&gt;
&lt;li&gt;Volume inline efficiency (compression/deduplication) enabled&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DiskIopsConfiguration&lt;/code&gt; at &lt;code&gt;AUTOMATIC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reads slightly exceeding the read cache's capacity&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  More on item 5 — the read cache's two-tier structure
&lt;/h3&gt;

&lt;p&gt;Item 5 is covered in detail because it bears directly on this article's figures.&lt;/p&gt;

&lt;p&gt;FSx for ONTAP's file server has &lt;strong&gt;two tiers of read cache: in-memory and NVMe&lt;/strong&gt;&lt;br&gt;
(&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/performance.html" rel="noopener noreferrer"&gt;performance specs&lt;/a&gt;). For&lt;br&gt;
ap-northeast-1's first-generation Single-AZ, what's published differs by tier.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Capacity at 2048 MBps&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-memory&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;256 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Performance specs, the "other regions" table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVMe read cache&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not stated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No column exists in that same table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For NVMe, only a condition is stated in a separate section: "A Single-AZ 1 created on or after&lt;br&gt;
2022-11-28 with a throughput capacity of 2 GBps or more gets a read cache." This file system meets&lt;br&gt;
that condition. &lt;strong&gt;Checking with the ONTAP CLI directly confirmed it was enabled.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;128 MBps configuration : system node external-cache -&amp;gt; 0 records
2048 MBps configuration: is_enabled: true (both nodes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And how SSD IOPS gets used is stated explicitly in the same documentation. &lt;strong&gt;SSD IOPS is used only&lt;br&gt;
when reading data that's in neither the in-memory cache nor the NVMe cache.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With that in mind, the measurement can be read this way. A 280 GiB read exceeds the in-memory&lt;br&gt;
cache's 256 GB (= 238 GiB) by 18%, but there's an NVMe read cache behind it. This&lt;br&gt;
configuration's NVMe capacity isn't published (in the four regions with a column, it's 1,900 GB at&lt;br&gt;
the same 2048 MBps). &lt;strong&gt;The measured 98.5-99.9% of read bytes not going to disk is consistent with&lt;br&gt;
coming back from these two tiers&lt;/strong&gt; (breakdown under "Per-tool visibility" below).&lt;/p&gt;

&lt;p&gt;That's why the observation "raising SSD IOPS from 3,072 to 40,000 made reads 7.14x faster"&lt;br&gt;
didn't square with the documentation. If it's coming back from cache, SSD IOPS shouldn't matter.&lt;/p&gt;

&lt;p&gt;That mismatch went away on re-measurement (2026-09-17). Setting the working set to 512 GiB —&lt;br&gt;
2.15x the in-memory cache's 238 GiB — and disabling the NVMe cache, re-measuring showed&lt;br&gt;
96-126% of read bytes coming from disk, with the low end saturating at &lt;code&gt;DiskIopsUtilization&lt;/code&gt;&lt;br&gt;
112%. &lt;strong&gt;"SSD IOPS determines the disk throughput level" is consistent with measurement under&lt;br&gt;
this condition.&lt;/strong&gt; So the mismatch's cause wasn't the documentation — it was my measurement:&lt;br&gt;
a 280 GiB working set only exceeded the cache by 18%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ratio itself changed, though.&lt;/strong&gt; 256.47 -&amp;gt; 1,246.67 MB/s, 4.86x. 7.14x doesn't&lt;br&gt;
reproduce. And what's capping the high side, around 1,250 MB/s, is none of the published&lt;br&gt;
utilization metrics (disk throughput 67% / IOPS 43% / network 43%). It isn't concurrency&lt;br&gt;
either — going from 8 to 32 to 64 threads drops it from 1,071 to 999 to 958, with response time&lt;br&gt;
alone scaling proportionally (6.9 -&amp;gt; 31.0 -&amp;gt; 59.3 ms). This is left unresolved.&lt;/p&gt;

&lt;p&gt;To check in your own environment, the order is two steps. Check for an NVMe cache with&lt;br&gt;
&lt;code&gt;system node external-cache&lt;/code&gt;, and then read a volume that reliably exceeds the sum of both&lt;br&gt;
tiers in one pass. &lt;strong&gt;A first-generation Single-AZ at 2 GBps or more gets an NVMe cache by&lt;br&gt;
default — there's no way to prevent it from being created, only to create it and then disable&lt;br&gt;
it.&lt;/strong&gt; I forgot this once and measured four points with it left enabled.&lt;/p&gt;
&lt;h3&gt;
  
  
  Item 6 — SMB Multichannel disabled by default
&lt;/h3&gt;

&lt;p&gt;ONTAP's SMB Multichannel is disabled by default. This is a separate setting from SMB 3.x&lt;br&gt;
itself, so a channel can stay at one even while negotiated at &lt;code&gt;dialect=3.1.1&lt;/code&gt;. That state held in&lt;br&gt;
this measurement environment: while one host was reading, only one established TCP connection to&lt;br&gt;
the SVM existed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enabling it doesn't reach connections already established.&lt;/strong&gt; Enabling only affects new&lt;br&gt;
connections. Removing and re-mapping the drive kept the same local port, and it only became four&lt;br&gt;
channels after restarting the SMB client service.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;max_connections_per_session&lt;/code&gt; is a separate value. &lt;strong&gt;With Multichannel disabled, setting it to 32&lt;br&gt;
still yields one channel, and you can misread "raised the limit but it didn't increase" as a&lt;br&gt;
result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mechanism, sources, and where to read channel count are in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/reference/limits/smb-multichannel-enablement.md" rel="noopener noreferrer"&gt;SMB Multichannel is disabled by default, and enabling it doesn't reach already-established connections&lt;/a&gt;&lt;br&gt;
(Japanese).&lt;/p&gt;
&lt;h2&gt;
  
  
  Three ceilings that limit writes on the FSx for ONTAP side
&lt;/h2&gt;

&lt;p&gt;Before getting into the three-path comparison, here's what limits this architecture's own side.&lt;br&gt;
&lt;strong&gt;There's a ceiling that doesn't move even when throughput capacity is raised, and not knowing it&lt;br&gt;
leads to a sizing miss.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One term first. Throughput capacity is a value — 128 / 256 / 512 / 1024 / 2048 MBps and so on —&lt;br&gt;
that you set on the file system. It isn't metered usage; you're billed by time against the&lt;br&gt;
value you set. This article measures at two points, 128 MBps and 2048 MBps.&lt;/p&gt;

&lt;p&gt;For ap-northeast-1's first-generation Single-AZ, three ceilings bind on writes, and &lt;strong&gt;the one that&lt;br&gt;
actually applies is whichever is smallest.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ceiling&lt;/th&gt;
&lt;th&gt;This environment's value&lt;/th&gt;
&lt;th&gt;How it's determined&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Provisioned throughput capacity&lt;/td&gt;
&lt;td&gt;128 / 2048 MBps&lt;/td&gt;
&lt;td&gt;Set by you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk throughput out of SSD&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;768 MBps&lt;/strong&gt; (768 MBps/TiB x 1 TiB, when SSD IOPS is &lt;code&gt;AUTOMATIC&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Driven by the provisioned SSD IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-HA-pair ceiling&lt;/td&gt;
&lt;td&gt;Write &lt;strong&gt;750 MBps&lt;/strong&gt; (read is 2,048 MBps)&lt;/td&gt;
&lt;td&gt;Determined by the file system's generation and configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: &lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/performance.html" rel="noopener noreferrer"&gt;performance specs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second row isn't a fixed value determined by SSD capacity alone.&lt;/strong&gt; 768 MBps/TiB is what you&lt;br&gt;
get when SSD IOPS is set to &lt;code&gt;AUTOMATIC&lt;/code&gt; (3 IOPS/GiB). Raising SSD IOPS with &lt;code&gt;USER_PROVISIONED&lt;/code&gt;&lt;br&gt;
raises that 768 MBps/TiB figure right along with it. Conversely, raising only throughput&lt;br&gt;
capacity to 2048 MBps while leaving SSD IOPS at &lt;code&gt;AUTOMATIC&lt;/code&gt; leaves disk throughput capped at 768&lt;br&gt;
MBps/TiB. There are two levers, and raising only one doesn't move the ceiling.&lt;/p&gt;

&lt;p&gt;Laying out which configuration hits which ceiling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Throughput capacity&lt;/th&gt;
&lt;th&gt;Ceiling that applied&lt;/th&gt;
&lt;th&gt;Measured (uncompressible, 8 MiB, concurrency 64)&lt;/th&gt;
&lt;th&gt;Against that ceiling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;128 MBps&lt;/td&gt;
&lt;td&gt;The provisioned 128 MBps&lt;/td&gt;
&lt;td&gt;133.4 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;104%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2048 MBps&lt;/td&gt;
&lt;td&gt;The per-HA-pair write ceiling, 750 MBps&lt;/td&gt;
&lt;td&gt;415 MB/s (measured twice: 415.1 / 415.7)&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Landing at 104% of the 128 MBps configuration — slightly over the provisioned value — is a&lt;br&gt;
burst effect.&lt;/strong&gt; This configuration's disk throughput can sustain 128 MBps continuously, or 600&lt;br&gt;
MBps briefly. Bursting was available but the measurement stopped at 133.4, so the provisioned&lt;br&gt;
throughput capacity was the binding ceiling in this configuration.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On second-generation, the meaning of the provisioned value itself is different.&lt;/strong&gt; In a separate&lt;br&gt;
measurement, a second-generation Single-AZ's provisioned value points to a disk throughput&lt;br&gt;
&lt;strong&gt;baseline&lt;/strong&gt;, and the network-side baseline is roughly twice that. On a configuration provisioned&lt;br&gt;
at 1,536 MBps, 1 MiB sequential reads produced &lt;strong&gt;2,882 MB/s for 27 minutes, then 1,439 MB/s.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Carrying this table's first-generation reading over to second-generation misses a read sizing&lt;br&gt;
estimate by 2x&lt;/strong&gt;&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-capacity-burst-and-baseline.md" rel="noopener noreferrer"&gt;measurement record&lt;/a&gt;)&lt;br&gt;
(Japanese).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the 2048 MBps configuration, the provisioned value stopped being the ceiling. The denominator&lt;br&gt;
for percentage changes here, so both are given. 415 MB/s is 20% against the provisioned 2048&lt;br&gt;
MBps. &lt;strong&gt;Against the per-HA-pair write ceiling of 750 MBps that actually applied, it's 55%.&lt;/strong&gt; The&lt;br&gt;
surprise of "provisioned 2048 MBps, only got 415" is explained by the provisioned value no longer&lt;br&gt;
being the applicable ceiling.&lt;/p&gt;

&lt;p&gt;What's consuming the remaining 45% — the unused portion of the 750 MBps — hasn't been isolated.&lt;br&gt;
Two candidates come from the published specs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writes use twice the network bandwidth of reads.&lt;/strong&gt; This is documented explicitly, because a
write replicates to the secondary file server, so a single write turns into twice the network
throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fixed cost per request.&lt;/strong&gt; Raising concurrency only stretched p50 (median latency) out to
1054.9 ms (at concurrency 64). If there's a fixed cost per single request, raising concurrency
doesn't raise requests-per-second. &lt;strong&gt;If request rate doesn't rise, total throughput doesn't
rise either.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which one is doing how much hasn't been isolated. This isn't a case where "the reason it doesn't&lt;br&gt;
reach 750 is this one thing" can be stated.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bottlenecks sitting in different places per path
&lt;/h2&gt;

&lt;p&gt;This is the main point. Three numbers that come out in the same unit, MB/s, were each plateauing&lt;br&gt;
somewhere different. &lt;strong&gt;The figure's A / B / C map in that order.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Where the bottleneck sits&lt;/th&gt;
&lt;th&gt;What increases it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;This architecture (FSx for ONTAP S3 Access Point)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Over the S3 API path&lt;/strong&gt;, the &lt;strong&gt;provisioned&lt;/strong&gt; throughput capacity on the file system, shared across all clients&lt;/td&gt;
&lt;td&gt;Raise the provisioned value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3&lt;/td&gt;
&lt;td&gt;Not the service side — &lt;strong&gt;client-side network bandwidth&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Add more clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3 Files&lt;/td&gt;
&lt;td&gt;The CPU of the &lt;strong&gt;proxy process&lt;/strong&gt; running on the client&lt;/td&gt;
&lt;td&gt;Use a bigger client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: the row above is scoped to the S3 API path. It's based on measurement (1.01x with 4&lt;br&gt;
clients). &lt;strong&gt;On the file path (NFS/SMB) of the same file system, reads exceeded the provisioned&lt;br&gt;
value of 6,144 MBps by more than 2x.&lt;/strong&gt; Reads served from cache don't go through disk, so stating&lt;br&gt;
"the provisioned value is the ceiling" without naming the path would contradict that&lt;br&gt;
measurement. Details in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md" rel="noopener noreferrer"&gt;per-protocol measurement results&lt;/a&gt;&lt;br&gt;
(Japanese).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the decision flow for telling which of the above a measurement is hitting, in your own&lt;br&gt;
environment.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    START["A measured value plateaus"] --&amp;gt; Q1{"Does the total grow&amp;lt;br/&amp;gt;when adding clients?"}
    Q1 --&amp;gt;|"No"| C1["Provisioned throughput capacity&amp;lt;br/&amp;gt;on the file system&amp;lt;br/&amp;gt;(this architecture's S3 API path)"]
    Q1 --&amp;gt;|"Yes"| Q2{"Is the per-client value&amp;lt;br/&amp;gt;near the client's network bandwidth?"}
    Q2 --&amp;gt;|"Yes"| C2["Client-side network bandwidth&amp;lt;br/&amp;gt;(Amazon S3)"]
    Q2 --&amp;gt;|"No"| Q3{"Is CPU high on a process&amp;lt;br/&amp;gt;running on the client?"}
    Q3 --&amp;gt;|"Yes"| C3["CPU of a proxy process&amp;lt;br/&amp;gt;on the client&amp;lt;br/&amp;gt;(Amazon S3 Files)"]
    Q3 --&amp;gt;|"No"| C4["A factor outside these three.&amp;lt;br/&amp;gt;Isolate it separately"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The figure states the same thing as the table below. &lt;strong&gt;A single-host measurement alone can't&lt;br&gt;
determine the type&lt;/strong&gt; — telling them apart needs either adding clients, or holding transfer size&lt;br&gt;
and concurrency fixed while varying something else at one host (details under&lt;br&gt;
a single-client figure that was actually the client's own limit&lt;br&gt;
above).&lt;/p&gt;
&lt;h3&gt;
  
  
  FSx for ONTAP — a shared ceiling that wasn't one thing
&lt;/h3&gt;

&lt;p&gt;This is the left side of the figure's panel A, the S3 API write side. &lt;strong&gt;Adding clients doesn't&lt;br&gt;
raise the total.&lt;/strong&gt; With the same setting and varying client count, 4 clients produced 1.01x.&lt;br&gt;
Since the total doesn't change, each client's share is roughly 1/n of it.&lt;/p&gt;

&lt;p&gt;The reason is that the ceiling is a value provisioned on the file system. &lt;strong&gt;This is a&lt;br&gt;
configuration where capacity planning is done per file system, not per client.&lt;/strong&gt; If more clients&lt;br&gt;
are expected, plan for it on the throughput capacity side.&lt;/p&gt;

&lt;p&gt;Note that reads came out higher than the provisioned value. On a 128 MBps configuration, reads&lt;br&gt;
reached 579.3 MB/s (8 MiB, concurrency 16), &lt;strong&gt;4.5x the provisioned value.&lt;/strong&gt; Using the write-side&lt;br&gt;
ceiling to size reads will miss.&lt;/p&gt;
&lt;h4&gt;
  
  
  How the ceiling splits as client count grows to 8
&lt;/h4&gt;

&lt;p&gt;The result above, up to 4 clients, is first-generation with the provisioned value as the ceiling.&lt;br&gt;
&lt;strong&gt;Scaling to 8 clients / 128 connections on second-generation, with the same file system, same&lt;br&gt;
setting, and same connection count, the total swung 5.5x.&lt;/strong&gt; The only thing changed was "where each&lt;br&gt;
host reads from."&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7j7ut1k3bvkcpp5xc91.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7j7ut1k3bvkcpp5xc91.png" alt="The two ceilings a read can hit" width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With every host reading the same file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hosts&lt;/th&gt;
&lt;th&gt;Connections&lt;/th&gt;
&lt;th&gt;Client total&lt;/th&gt;
&lt;th&gt;Port-measured&lt;/th&gt;
&lt;th&gt;Per host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2,664.58 MB/s&lt;/td&gt;
&lt;td&gt;2,698.7 MiB/s&lt;/td&gt;
&lt;td&gt;2,664.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;8,562.94 MB/s&lt;/td&gt;
&lt;td&gt;8,635.2 MiB/s&lt;/td&gt;
&lt;td&gt;2,140.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11,916.29 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12,173.0 MiB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,489.54&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the working set held at a fixed 600 GiB, split into 75 GiB per host at 8 hosts, &lt;strong&gt;so no block&lt;br&gt;
is shared:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hosts&lt;/th&gt;
&lt;th&gt;Connections&lt;/th&gt;
&lt;th&gt;Client total&lt;/th&gt;
&lt;th&gt;Port-measured&lt;/th&gt;
&lt;th&gt;Per host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;2,299.05 MB/s&lt;/td&gt;
&lt;td&gt;2,323.4 MiB/s&lt;/td&gt;
&lt;td&gt;1,149.53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,173.37 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,191.7 MiB/s&lt;/td&gt;
&lt;td&gt;271.67&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Quadrupling connections from 32 to 128 doesn't grow the total.&lt;/strong&gt; It's already saturated at 32.&lt;/p&gt;

&lt;p&gt;The difference comes down to how much is served from the file server's read cache. SSD IOPS is&lt;br&gt;
used only when reading data in neither the in-memory nor the NVMe cache, per the&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/performance.html" rel="noopener noreferrer"&gt;performance specs&lt;/a&gt;. Overlap&lt;br&gt;
in read ranges pulls toward the former; no overlap pulls toward the latter. &lt;strong&gt;There isn't one&lt;br&gt;
ceiling — there are two, and which one a workload hits depends on the workload.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  A hypothesis that turned out wrong
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Before this measurement, I had assumed the SVM's single data LIF was a roughly 4,400 MB/s&lt;br&gt;
ceiling. That was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since client-reported totals aren't evidence on their own, I cross-checked against ONTAP's&lt;br&gt;
physical port cumulative counters (&lt;code&gt;nic_common&lt;/code&gt;'s &lt;code&gt;transmit_bytes&lt;/code&gt;), differenced. The&lt;br&gt;
port-measured value exceeded the client total by within 1.3% at every point, and all 8 points went&lt;br&gt;
through the same single port. &lt;strong&gt;That means a single LIF was carrying 12,173 MiB/s — 102 Gbps.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One thing tripped up this cross-check. &lt;strong&gt;The instantaneous-value counter returned 0 even while&lt;br&gt;
traffic was actually flowing.&lt;/strong&gt; Only the differenced cumulative value was usable.&lt;/p&gt;

&lt;p&gt;This changes how capacity planning should work. &lt;strong&gt;Rather than counting hosts, decide first whether&lt;br&gt;
each host's read range overlaps.&lt;/strong&gt; A non-overlapping design pulls toward the disk path's ceiling,&lt;br&gt;
so adding hosts doesn't grow the total — that's what the 32-connection and 128-connection rows&lt;br&gt;
above show.&lt;/p&gt;
&lt;h4&gt;
  
  
  A single-client figure that was actually the client's own limit
&lt;/h4&gt;

&lt;p&gt;A second, similarly shaped mistake, this time by me. In a separate round, on a first-generation&lt;br&gt;
2,048 MBps configuration, I got roughly 1,250 MB/s and &lt;strong&gt;treated it as "this configuration's read&lt;br&gt;
ceiling."&lt;/strong&gt; I checked with two clients (2026-09-19).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First-generation 2,048 MBps, SSD 1,024 GiB, SSD IOPS 40,000 (&lt;code&gt;USER_PROVISIONED&lt;/code&gt;), NVMe read&lt;br&gt;
cache disabled, volume 800 GiB, client c5n.9xlarge x2, &lt;strong&gt;each reading a non-overlapping 300&lt;br&gt;
GiB&lt;/strong&gt;, 1 MiB sequential, 8 threads, &lt;code&gt;o_direct&lt;/code&gt;, ONTAP 9.18.1P6, ap-northeast-1.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Client total&lt;/th&gt;
&lt;th&gt;Disk throughput utilization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single host only&lt;/td&gt;
&lt;td&gt;1,195.27 MB/s&lt;/td&gt;
&lt;td&gt;63% (SSD IOPS also 40%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two hosts simultaneously&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;981.90 + 919.58 = 1,901.48 MB/s&lt;/strong&gt; (1.59x)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;102-103%&lt;/strong&gt; (burst)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;With a single host, the disk path was only at 63% — the file system side had headroom.&lt;/strong&gt; Only&lt;br&gt;
with two hosts did the disk side saturate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From here I chased "so what's the per-host ceiling?" The answer was two of my own settings, not&lt;br&gt;
a resource.&lt;/strong&gt; On the same file system, I varied &lt;code&gt;rsize&lt;/code&gt; to 1 MiB and thread count (the next day).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition (1 host)&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Disk throughput utilization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transfer 64 KiB, 8 threads&lt;/td&gt;
&lt;td&gt;1,195.27 MB/s&lt;/td&gt;
&lt;td&gt;6.69 ms&lt;/td&gt;
&lt;td&gt;63%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer 1 MiB, 8 threads&lt;/td&gt;
&lt;td&gt;1,446.66 MB/s&lt;/td&gt;
&lt;td&gt;5.443 ms&lt;/td&gt;
&lt;td&gt;77.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer 1 MiB, &lt;strong&gt;16 threads&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,646.17 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.208 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;102.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer 1 MiB, 32 threads&lt;/td&gt;
&lt;td&gt;1,647.42 MB/s&lt;/td&gt;
&lt;td&gt;16.420 ms&lt;/td&gt;
&lt;td&gt;102.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer 1 MiB, 64 threads&lt;/td&gt;
&lt;td&gt;1,648.71 MB/s&lt;/td&gt;
&lt;td&gt;32.838 ms&lt;/td&gt;
&lt;td&gt;102.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Transfer size added +21%, concurrency added another +14%, and then it stopped.&lt;/strong&gt; Past 16 threads,&lt;br&gt;
only response time keeps doubling while throughput doesn't move. It's queueing, not gaining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And a single host reaches the same 102.6% that two hosts reached.&lt;/strong&gt; IOPS sat at 50.9% and network&lt;br&gt;
at 66.3%, neither saturated. I could not name a specific client-side device that was saturated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This "can't name it" was traced to a different tier and resolved separately (2026-09-20).&lt;/strong&gt; On&lt;br&gt;
second-generation, 6,144 MBps / 200,000 IOPS, measuring &lt;strong&gt;five ways of splitting the working set&lt;/strong&gt;&lt;br&gt;
shows adding volumes moves it only &lt;strong&gt;+1.0%&lt;/strong&gt;, while &lt;strong&gt;adding SVMs moves it +26.3% and adding&lt;br&gt;
clients moves it +52.7%.&lt;/strong&gt; So what matters is &lt;strong&gt;the number of distinct IPs the client talks to&lt;br&gt;
and the client count&lt;/strong&gt;, not thread count (+8.9% going from 512 to 1,024). &lt;strong&gt;With two clients, the&lt;br&gt;
disk side reaches 96.6% of the provisioned value, and only then does the file system side become&lt;br&gt;
the actual limiter.&lt;/strong&gt; No single device can be named because the limiter isn't one counter — it's&lt;br&gt;
&lt;strong&gt;how receive processing is distributed&lt;/strong&gt;&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md#4-kib-%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E8%AA%AD%E3%81%BF%E3%81%AE%E5%A4%A9%E4%BA%95%E3%81%AE%E6%89%80%E5%9C%A82026-09-20" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)&lt;br&gt;
(Japanese).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This reinforces this article's claim. The table at the top says to confirm the type of ceiling;&lt;br&gt;
a single-host measurement can't determine that type. &lt;strong&gt;Using a value measured alone as a ceiling&lt;br&gt;
for capacity planning under-buys.&lt;/strong&gt; There are two ways to check — adding hosts and fixing transfer&lt;br&gt;
size/concurrency — and both arrive at the same conclusion.&lt;/p&gt;

&lt;p&gt;I also chased the discrepancy between them, without using any additional resources. Both file&lt;br&gt;
systems had already been deleted, but &lt;strong&gt;CloudWatch retains data for 63 days after deletion.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This architecture doesn't publish a single burst-remaining metric&lt;/strong&gt; (zero out of 77 metrics
contain &lt;code&gt;Balance&lt;/code&gt;). My earlier explanation, "because burst remainders weren't aligned," doesn't
hold, because nothing to align exists.&lt;/li&gt;
&lt;li&gt;The denominator for utilization was the provisioned value itself (2,048 MB/s).
&lt;code&gt;DiskReadBytes / (utilization / 100)&lt;/code&gt; converges to 2,048 across every minute. &lt;strong&gt;On
first-generation, over 100% just means "more than the provisioned value came out," not evidence
of spending a burst allowance.&lt;/strong&gt; Second-generation's 1,536 config at 203.5% is against a
baseline, so the same metric has a different denominator by generation.&lt;/li&gt;
&lt;li&gt;And both sessions were hitting the same server-side ceiling of about 2,040 MB/s. &lt;strong&gt;The 15%
client-reported difference wasn't a difference in what the file system delivered.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As one discrepancy closed, a second one — between two counter systems — opened, and it closed&lt;br&gt;
the same day too. At saturation, the server reported about 2,043 MB/s while vdbench reported&lt;br&gt;
1,646-1,649 (19% short). The cause was how the measurement was taken. Aligning the window's&lt;br&gt;
start to a minute boundary and comparing only the minutes fully contained in the window, the&lt;br&gt;
two systems agree within 0.2% (300s window: 271.04 vs 271.57; 900s window: 326.70 vs 326.07).&lt;br&gt;
The 19% arose from taking a 60-second average of a minute where load was only partially applied&lt;br&gt;
as the server-side value. Against a steady-state 271 MB/s, the start minute was 216.85 and the end&lt;br&gt;
minute 58.47. &lt;strong&gt;There's nothing to restate about FSx for ONTAP here.&lt;/strong&gt; Making the two agree&lt;br&gt;
requires both aligning the window and discarding the partial minutes&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md#%E7%AA%93%E3%82%92%E5%88%86%E5%A2%83%E7%95%8C%E3%81%AB%E5%90%88%E3%82%8F%E3%81%9B%E3%81%9F-2-%E7%B3%BB%E7%B5%B1%E3%81%AE%E6%AF%94%E8%BC%832026-09-20" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)&lt;br&gt;
(Japanese).&lt;/p&gt;
&lt;h3&gt;
  
  
  The shared ceiling split two ways over SMB too
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The same shape showed up over SMB.&lt;/strong&gt; Varying only client count across 8 clients didn't produce&lt;br&gt;
one single answer.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwgczwjvrkjttr0ncmvjc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwgczwjvrkjttr0ncmvjc.png" alt="The SMB host-count test's configuration" width="799" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything below the share is identical between the two.&lt;/strong&gt; Same single CIFS share, same SMB 3.1.1&lt;br&gt;
with 4 Multichannel channels, same physical port on the same node of the same SVM. Only what each&lt;br&gt;
host reads differs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Total at 8 hosts&lt;/th&gt;
&lt;th&gt;Read range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10,721.19 MB/s (port-measured 11,194.7 MiB/s)&lt;/td&gt;
&lt;td&gt;Every host reads the same range of the same file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,225.31 MB/s&lt;/td&gt;
&lt;td&gt;Each host reads its own 1/8 of the range&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The former was still climbing at 8 hosts; the latter plateaued between 4 and 8. The former's&lt;br&gt;
growth is the result of ONTAP's memory serving the overlap. &lt;strong&gt;Citing only one of them leads to the&lt;br&gt;
opposite conclusion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Client-side totals aren't evidence on their own here either. A single read can satisfy multiple&lt;br&gt;
clients, so bytes never sent over the wire can get counted. Every point here is cross-checked&lt;br&gt;
against ONTAP's physical port cumulative counters.&lt;/p&gt;

&lt;p&gt;Per-point figures, and what this measurement doesn't answer (the 180-second window doesn't&lt;br&gt;
separate burst from baseline), are in the&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md#smb-%E3%81%AE%E5%8F%B0%E6%95%B0%E8%A9%A6%E9%A8%931--4--8-%E5%8F%B0" rel="noopener noreferrer"&gt;SMB host-count test&lt;/a&gt;&lt;br&gt;
(Japanese).&lt;/p&gt;
&lt;h3&gt;
  
  
  Amazon S3 — a total proportional to client count
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The figure's panel B is made of just this one bidirectional arrow.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hosts&lt;/th&gt;
&lt;th&gt;Write total&lt;/th&gt;
&lt;th&gt;Per host&lt;/th&gt;
&lt;th&gt;Read total&lt;/th&gt;
&lt;th&gt;Per host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;581.5 MB/s&lt;/td&gt;
&lt;td&gt;581.5&lt;/td&gt;
&lt;td&gt;496.3 MB/s&lt;/td&gt;
&lt;td&gt;496.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1158.0 MB/s&lt;/td&gt;
&lt;td&gt;579.0&lt;/td&gt;
&lt;td&gt;977.5 MB/s&lt;/td&gt;
&lt;td&gt;488.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2362.5 MB/s&lt;/td&gt;
&lt;td&gt;590.6&lt;/td&gt;
&lt;td&gt;1974.7 MB/s&lt;/td&gt;
&lt;td&gt;493.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3601.4 MB/s&lt;/td&gt;
&lt;td&gt;600.2&lt;/td&gt;
&lt;td&gt;3038.5 MB/s&lt;/td&gt;
&lt;td&gt;506.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4783.2 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;597.9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3992.2 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;499.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Going from 1 to 8 clients, write totals grew 8.23x and reads 8.04x. The per-host value doesn't&lt;br&gt;
drop.&lt;/strong&gt; The total reached 38.3 Gbps at 8 hosts, and this range never showed a point where it&lt;br&gt;
stopped being proportional to host count. Each host used a distinct prefix, so per-prefix request&lt;br&gt;
ceilings weren't tested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The roughly 500 MB/s visible with a single host was not Amazon S3's ceiling — it was the&lt;br&gt;
client's.&lt;/strong&gt; Since adding hosts grows the total, capacity planning here comes down to counting&lt;br&gt;
client-side bandwidth.&lt;/p&gt;
&lt;h3&gt;
  
  
  Amazon S3 Files — the CPU of a proxy process on the client
&lt;/h3&gt;

&lt;p&gt;This path's structure differs from the other two, so the structure is described first. &lt;strong&gt;The&lt;br&gt;
figure's panel C shows the client and &lt;code&gt;efs-proxy&lt;/code&gt; sharing the same box — that's it.&lt;/strong&gt; Mounting S3&lt;br&gt;
Files starts a transfer process called &lt;code&gt;efs-proxy&lt;/code&gt; on the client, and the NFS client connects to&lt;br&gt;
that process. It's visible in the mount info.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;127.0.0.1:/ nfs4 rw,vers=4.2,rsize=1048576,wsize=1048576,proto=tcp,port=20812,addr=127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The mount target is &lt;code&gt;127.0.0.1&lt;/code&gt; — itself.&lt;/strong&gt; Data is then carried from &lt;code&gt;efs-proxy&lt;/code&gt; to the service&lt;br&gt;
side. FSx for ONTAP's mount target is the file system itself, so this is structurally different.&lt;/p&gt;

&lt;p&gt;With that structure in mind, I measured what&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;not supporting &lt;code&gt;nconnect&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
means in practice. &lt;code&gt;nconnect&lt;/code&gt; is an option that increases the number of TCP connections an NFS&lt;br&gt;
mount opens.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stream count&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;Write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;67.9 MB/s&lt;/td&gt;
&lt;td&gt;34.6 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;264.3 MB/s&lt;/td&gt;
&lt;td&gt;140.5 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;451.3 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;281.0 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;404.8 / 450.0 MB/s&lt;/td&gt;
&lt;td&gt;531.8 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Reads reach about 450 MB/s at 8 streams and don't grow further at 16&lt;/strong&gt; (writes are still growing&lt;br&gt;
at 16, 531.8 MB/s). What follows covers the read plateau. 450 MB/s is 3.6 Gbps, under the&lt;br&gt;
single-flow ceiling&lt;br&gt;
(&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/performance.html" rel="noopener noreferrer"&gt;roughly 5 Gbps per network flow&lt;/a&gt;).&lt;br&gt;
So the single-flow ceiling isn't what's binding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plateau sits at &lt;code&gt;efs-proxy&lt;/code&gt;'s CPU.&lt;/strong&gt; While reading at 16 streams, CPU usage on the&lt;br&gt;
&lt;code&gt;efs-proxy&lt;/code&gt; process was 67.3% + 18.1% + 14.9% (an 8-vCPU host).&lt;/p&gt;

&lt;p&gt;This is where the meaning of not supporting &lt;code&gt;nconnect&lt;/code&gt; becomes clear. What &lt;code&gt;nconnect&lt;/code&gt; increases&lt;br&gt;
is the connection count of the NFS mount — between the NFS client and &lt;code&gt;efs-proxy&lt;/code&gt;. &lt;strong&gt;Since the&lt;br&gt;
plateau is &lt;code&gt;efs-proxy&lt;/code&gt;'s own CPU, even if it were supported, it wouldn't reach where the plateau&lt;br&gt;
actually is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So it isn't "slow because &lt;code&gt;nconnect&lt;/code&gt; isn't supported." &lt;strong&gt;Since the bottleneck is a process's CPU on&lt;br&gt;
the client, the fix is a bigger client, not a mount option.&lt;/strong&gt; &lt;code&gt;efs-proxy --tls&lt;/code&gt; being on this path&lt;br&gt;
is the cost of a configuration where in-transit encryption is the default, not a defect.&lt;/p&gt;

&lt;p&gt;One practical note. Specifying &lt;code&gt;nconnect&lt;/code&gt; hung the mount (90-second timeout). Not a rejection,&lt;br&gt;
not silent ignoring. The first measurement was entirely lost to this. &lt;strong&gt;When trying an unsupported&lt;br&gt;
option, wrap it with &lt;code&gt;timeout&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Absence of decay over a 15-minute sustained write
&lt;/h2&gt;

&lt;p&gt;Writes had been measured in a 120-second window, so I re-measured at 900 seconds, on the&lt;br&gt;
second-generation environment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Steady-state window average&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2,063.00 MB/s&lt;/strong&gt; (900 seconds after a 60-second warmup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-second interval min / max&lt;/td&gt;
&lt;td&gt;1,749.0 / 2,128.4 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cumulative written&lt;/td&gt;
&lt;td&gt;Roughly &lt;strong&gt;1.77 TiB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decay&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not observed. The first minute and the last minute sit in the same range&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Absorption somewhere can be ruled out.&lt;/strong&gt; In-memory cache is 256 GB and NVMe cache is disabled;&lt;br&gt;
the amount written is 7x that. Volume efficiency was &lt;code&gt;disabled&lt;/code&gt;, with 0 savings.&lt;/p&gt;

&lt;p&gt;Stating the relationship to the published value directly: the documentation says for&lt;br&gt;
second-generation, "reads get the full throughput capacity, writes get one-third of it," and&lt;br&gt;
it separately names 6,144 MBps as an exception to that general rule, listing it in a separate&lt;br&gt;
table with Single-AZ writes at 1,024 MBps. &lt;strong&gt;So the published value that should apply to a 6,144&lt;br&gt;
configuration is 1,024 MBps.&lt;/strong&gt; The measurement was 2.05x that.&lt;/p&gt;

&lt;p&gt;This single point reproduced on a separate file system later. Same 6,144 MBps provisioned&lt;br&gt;
value, 1,800 seconds (twice this measurement's window), 2,097 MB/s, a 0.3% difference between&lt;br&gt;
the first 300 seconds and the last 300. &lt;strong&gt;Different day, different file system, different window&lt;br&gt;
length: 2,063 and 2,097, a 1.6% difference.&lt;/strong&gt; The no-decay conclusion is supported by two&lt;br&gt;
observations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lowering the provisioned value and measuring again still exceeds it in the same direction.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provisioned&lt;/th&gt;
&lt;th&gt;Published value that should apply&lt;/th&gt;
&lt;th&gt;Measured (sustained, median)&lt;/th&gt;
&lt;th&gt;Against the published value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,536&lt;/td&gt;
&lt;td&gt;One-third of the general rule -&amp;gt; &lt;strong&gt;512 MB/s&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,333 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.60x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6,144&lt;/td&gt;
&lt;td&gt;Exception table's &lt;strong&gt;1,024 MB/s&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;2,097 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.05x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Both points exceed the published write ceiling.&lt;/strong&gt; Same direction, ratios of 2.60 and 2.05.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This used to be described a different way.&lt;/strong&gt; It previously read "the general rule holds at&lt;br&gt;
6,144 and misses at 1,536," but &lt;strong&gt;6,144 / 3 = 2,048 being close to the measured 2,097 is a&lt;br&gt;
coincidence.&lt;/strong&gt; The documentation names 6,144 as an exception to the general rule, so &lt;strong&gt;applying&lt;br&gt;
the general rule there doesn't hold in the first place.&lt;/strong&gt; Corrected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which figure refers to what still isn't determined.&lt;/strong&gt; The table header reads "Maximum&lt;br&gt;
throughput &lt;strong&gt;from SSD storage&lt;/strong&gt;," which may refer to a ceiling scoped to the SSD path only.&lt;br&gt;
&lt;strong&gt;But this measurement closed off the likely absorption points&lt;/strong&gt; — the cumulative 1.77 TiB is&lt;br&gt;
7x the 256 GB in-memory cache, the NVMe cache was confirmed disabled on both nodes, efficiency&lt;br&gt;
was &lt;code&gt;disabled&lt;/code&gt; with 0 savings. &lt;strong&gt;Why it still comes out roughly 2x can't be explained from this&lt;br&gt;
side.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The design-level effect is over-, not under-provisioning. Working backward from the general&lt;br&gt;
rule to hit a write of 1,333 MB/s looks like it needs a 4,000 MBps-class provisioned value, but&lt;br&gt;
1,536 was measured to be enough. The second-generation provisioned rate is $2.013/MBps-month&lt;br&gt;
(ap-northeast-1, fetched 2026-09-04), so &lt;strong&gt;this misreading translates directly into overbuying.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only two points were measured: Single-AZ, 1 HA pair.&lt;/strong&gt; Multi-AZ, 2+ pairs, and provisioned values&lt;br&gt;
other than 1,536 and 6,144 weren't measured&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-capacity-burst-and-baseline.md" rel="noopener noreferrer"&gt;measurement record&lt;/a&gt;)&lt;br&gt;
(Japanese).&lt;/p&gt;

&lt;h3&gt;
  
  
  Capacity growth from continued writing
&lt;/h3&gt;

&lt;p&gt;Trying to continue into a read-side measurement after this, &lt;strong&gt;it didn't complete. The cause is a&lt;br&gt;
documented specification — a gap in my own design, not a documentation error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before starting, snapshot count was 0 and the snapshot policy was &lt;code&gt;none&lt;/code&gt;. 30 minutes into the run, a&lt;br&gt;
snapshot named starting with &lt;code&gt;backup-&lt;/code&gt; appeared, holding 739 GiB, and the write rate dropped from&lt;br&gt;
2,200 MB/s to 267 MB/s. Volume usage went from 88% to 100%, with 9.9 GiB free. &lt;strong&gt;Deleting files&lt;br&gt;
with &lt;code&gt;rm&lt;/code&gt; didn't return the free space.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cause is the automatic daily backup.&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/using-backups.html" rel="noopener noreferrer"&gt;Volume backups&lt;/a&gt; state that a&lt;br&gt;
backup first takes a volume snapshot, and &lt;strong&gt;that snapshot sits inside the volume, consuming&lt;br&gt;
capacity, and stays until the next backup.&lt;/strong&gt; For an overwrite-heavy workload, the pre-overwrite&lt;br&gt;
blocks are retained, so the amount written becomes capacity as-is.&lt;/p&gt;

&lt;p&gt;Why it doesn't return on deletion is&lt;br&gt;
&lt;a href="https://repost.aws/knowledge-center/fsx-ontap-correct-snapshot-spill" rel="noopener noreferrer"&gt;also documented&lt;/a&gt;.&lt;br&gt;
&lt;strong&gt;Data deleted from the active file system is not released as long as a snapshot still references&lt;br&gt;
it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Checking for snapshots before starting doesn't prevent this. Before the run it reads normally,&lt;br&gt;
because no snapshot exists yet. There was a recovery mechanism, though.&lt;br&gt;
&lt;a href="https://kb.netapp.com/on-prem/ontap/Ontap_OS/OS-KBs/What_is_volume_autosize_in_Data_ONTAP" rel="noopener noreferrer"&gt;It's documented&lt;/a&gt;&lt;br&gt;
that &lt;code&gt;volume autosize&lt;/code&gt; and Snapshot autodelete are designed to work together. Neither was active&lt;br&gt;
by default in this setup. The actual situation was that the default was in effect because&lt;br&gt;
neither was specified in the template. &lt;strong&gt;"Not specified" isn't "disabled."&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  15 minutes over SMB
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No decay over SMB either.&lt;/strong&gt; The 900-second steady-state window averaged 1,488.03 MB/s, with a&lt;br&gt;
10-second-interval min of 1,452.70 and max of 1,514.40; the first-third average was 1,487.55&lt;br&gt;
against a last-third of 1,486.57.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changing the client from an 8-vCPU type to a 36-vCPU type only moved it 0.6%&lt;/strong&gt; (1,478.95 versus&lt;br&gt;
1,488.03). This value isn't determined on the client side.&lt;/p&gt;

&lt;p&gt;The initial observation that a 300-second measurement (1,698.42 MB/s) was 12.4% higher than the&lt;br&gt;
900-second one doesn't reproduce (re-measured 2026-09-20). Varying only window length at 6,144&lt;br&gt;
MBps, the longer window actually came out 4.0% higher (1,375.4 versus 1,430.3 MB/s). Channel count&lt;br&gt;
stayed constant regardless of measurement duration, and even writes moved only 1.9% across window&lt;br&gt;
lengths. &lt;strong&gt;The original 12.4% gap looks like a one-off value, not a property of window length.&lt;/strong&gt;&lt;br&gt;
That said, the longer window's vdbench format was interrupted partway through, so it wasn't a&lt;br&gt;
comparable pair&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md#smb-%E3%81%AE%E7%AA%93%E9%95%B7-124-%E3%81%AE%E5%86%8D%E7%8F%BE%E4%B8%8D%E6%88%90%E7%AB%8B2026-09-20" rel="noopener noreferrer"&gt;re-measurement record&lt;/a&gt;)&lt;br&gt;
(Japanese).&lt;/p&gt;

&lt;p&gt;Against the same physical port on the same node of the same file system, NFS measured 2,063.00&lt;br&gt;
MB/s and SMB 1,488.03 MB/s. Since the source of concurrency differs (16 connections specified by&lt;br&gt;
the client versus 4 negotiated channels), this can't be attributed to a difference in the&lt;br&gt;
protocols themselves.&lt;/p&gt;

&lt;p&gt;Conditions and the 10-second-interval progression are in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md#smb-%E3%81%AE%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%81%BF%E3%81%AE%E6%8C%81%E7%B6%9A15-%E5%88%86" rel="noopener noreferrer"&gt;the SMB sustained write&lt;/a&gt;&lt;br&gt;
(Japanese).&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional read capacity via FlexCache
&lt;/h2&gt;

&lt;p&gt;One more thing was measured on the read side. This section covers the figure's panel A, where&lt;br&gt;
Origin and Cache sit as separate file systems. &lt;strong&gt;FlexCache isn't a mechanism for sharing the&lt;br&gt;
origin's (the replication source volume's) performance — it's a mechanism that adds separate&lt;br&gt;
capacity on the read side.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement point&lt;/th&gt;
&lt;th&gt;Measured, uncompressible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FlexCache first read (cache not yet filled; read while filling)&lt;/td&gt;
&lt;td&gt;306.8 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexCache resident (second read onward; cache already filled)&lt;/td&gt;
&lt;td&gt;882.7 / 907.1 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct origin read (control — the comparison baseline; already read once and cached)&lt;/td&gt;
&lt;td&gt;383.3 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Resident is 2.31x a direct origin read.&lt;/strong&gt; The cache is a separate file system from the origin,&lt;br&gt;
with its own throughput capacity, its own memory, and its own burst allowance. So while reading&lt;br&gt;
from the cache, the origin's own ceiling doesn't apply.&lt;/p&gt;

&lt;p&gt;The cost is the first read. &lt;strong&gt;Data that's never been read comes in at 306.8 the first time — 1/2.88&lt;br&gt;
of resident.&lt;/strong&gt; That's the figure to design around.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One qualification.&lt;/strong&gt; This was measured with both origin and cache at 128 MBps. Raising the&lt;br&gt;
origin to 2048 MBps puts a direct origin read into the 2000 MB/s class, so &lt;strong&gt;depending on&lt;br&gt;
configuration, the 2.31x could reverse. Unmeasured.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bottleneck shift with small objects
&lt;/h2&gt;

&lt;p&gt;Raising throughput capacity 16x and SSD IOPS 13x didn't raise request rate. The six measured&lt;br&gt;
points were 4 KiB reads and writes at concurrency 64 and 256, and 64 KiB reads and writes at&lt;br&gt;
concurrency 64. &lt;strong&gt;None of the six grew, and writes dropped 25%.&lt;/strong&gt; Against 40,000 IOPS, 279-605&lt;br&gt;
req/s — 0.7-1.5% of provisioned IOPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bottleneck sits on the S3 Access Point's request-handling side.&lt;/strong&gt; For designs that handle a&lt;br&gt;
large volume of small objects, raising throughput capacity and SSD IOPS doesn't help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-tool visibility
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's available to check "what is this actually limited by" while measuring differs by path.&lt;/strong&gt;&lt;br&gt;
Not knowing this means missing the numbers needed to identify a cause.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want to see&lt;/th&gt;
&lt;th&gt;Reaches it&lt;/th&gt;
&lt;th&gt;Doesn't reach it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per-volume throughput&lt;/td&gt;
&lt;td&gt;ONTAP's volume counters&lt;/td&gt;
&lt;td&gt;Client-side measurement (turns into a single-TCP-flow ceiling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node CPU&lt;/td&gt;
&lt;td&gt;CloudWatch (&lt;code&gt;AWS/FSx&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fsxadmin&lt;/code&gt;'s REST doesn't reach it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraction that went to disk&lt;/td&gt;
&lt;td&gt;CloudWatch's &lt;code&gt;DiskReadBytes&lt;/code&gt; / &lt;code&gt;DataReadBytes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Can't be determined from the client side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Files propagation lag&lt;/td&gt;
&lt;td&gt;CloudWatch (&lt;code&gt;AWS/S3/Files&lt;/code&gt;'s &lt;code&gt;ExportAge&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Measuring from the client side turns into roughly 590 MB/s. Linux's NFS mount opens only one&lt;br&gt;
TCP connection per server by default, and that's what limits it first. &lt;strong&gt;If you want to measure&lt;br&gt;
the storage-side ceiling, read the storage-side counters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CPU on both paths was around 22%. &lt;strong&gt;The NFS path delivers twice this architecture's throughput at&lt;br&gt;
the same CPU level.&lt;/strong&gt; So this architecture's cost isn't inside ONTAP — it's upstream, on the S3&lt;br&gt;
translation side.&lt;/p&gt;

&lt;p&gt;And on reads, &lt;code&gt;DiskReadBytes&lt;/code&gt; / &lt;code&gt;DataReadBytes&lt;/code&gt; was 1.4 / 0.9 / 1.5%. &lt;strong&gt;98.5-99.9% of read bytes&lt;br&gt;
didn't go to disk.&lt;/strong&gt; That means it came back from cache, which links directly to Part 5's fifth&lt;br&gt;
default.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS documentation notes and support inquiries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This article touches three points in the published specs. Two were filed on 2026-09-17.&lt;/strong&gt; The&lt;br&gt;
source for all three is&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/performance.html" rel="noopener noreferrer"&gt;the performance specs&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Second-generation writes exceed the published ceiling at both points measured (2.60x the general rule's 512 at a 1,536 provisioned value; 2.05x the exception table's 1,024 at a 6,144 provisioned value)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gap between published value and measurement.&lt;/strong&gt; Likely absorption points (two-tier cache, efficiency) closed off&lt;/td&gt;
&lt;td&gt;Filed (2026-09-17). &lt;strong&gt;AWS's reply (2026-09-24): the "up to a third" figure is not a hard limit but a sizing indicator.&lt;/strong&gt; A write uses bandwidth twice over — once to the active server, once to sync the standby server — but network burst capacity can push measured throughput above a third of the provisioned value, and AWS considers that expected behavior, not an anomaly. The table heading's "Maximum" is described as the ceiling to expect once burst is not a factor and the same file system is shared with other workloads. For sizing, AWS pointed back to the original rule of thumb: provision three times the required write throughput (e.g., a 1,333 MB/s requirement calls for a 6,144 MBps configuration)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ap-northeast-1's NVMe read cache capacity is missing from the table (the four regions with a column have it listed). The management page is also written scoped to "second-generation" only, so a first-generation user with a qualifying config can't tell they have a cache&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Missing content.&lt;/strong&gt; Presence was confirmable via ONTAP; capacity was not&lt;/td&gt;
&lt;td&gt;Filed (2026-09-17). &lt;strong&gt;AWS's reply (2026-09-21): no documentation change.&lt;/strong&gt; AWS's position is that a first-generation Single-AZ configuration at 2,048 MBps lacking a cache is not something the documentation promises against, and that enumerating every such corner case isn't practical, so the existing wording stands. Pointed instead to &lt;code&gt;system node external-cache show&lt;/code&gt; for checking presence and capacity per file system. &lt;strong&gt;Both the capacity-publication ask and the management-page scope ask were effectively declined&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The observation that raising SSD IOPS made reads 7.14x faster didn't square with "SSD IOPS is used only for reads not served from cache"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;My own measurement gap.&lt;/strong&gt; Hadn't read a volume that reliably exceeded the sum of both cache tiers&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not filed.&lt;/strong&gt; Re-measuring 2026-09-17 with the working set at 2.15x the cache size squared with the documentation (ratio came out 4.86x; 7.14x doesn't reproduce). &lt;strong&gt;Not a documentation error&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why the third isn't filed. The mismatch's cause was on my side. Re-measuring settled it&lt;br&gt;
(2026-09-17). With the working set at 512 GiB — 2.15x the in-memory cache — and the NVMe cache&lt;br&gt;
disabled, 96-126% of read bytes came from disk, with the low end saturating at&lt;br&gt;
&lt;code&gt;DiskIopsUtilization&lt;/code&gt; 112%. &lt;strong&gt;This matches the documentation. Not a documentation error, so not&lt;br&gt;
filed.&lt;/strong&gt; Only the ratio changed — 4.86x, and 7.14x doesn't reproduce. And what's capping the&lt;br&gt;
high side, roughly 1,250 MB/s, remains unresolved — that isn't a finding against the published&lt;br&gt;
spec, it's a range I haven't measured.&lt;/p&gt;

&lt;p&gt;The first and second were filed. &lt;strong&gt;Both were filed without me deciding "the correct value is&lt;br&gt;
this"&lt;/strong&gt; — as a gap or omission for whoever holds the values to judge. The first was framed as the&lt;br&gt;
question "what does 'Maximum throughput from SSD storage (write)' actually bound?" and the second&lt;br&gt;
as two points: publishing the capacity, and the management page's stated scope.&lt;/p&gt;

&lt;p&gt;The first used to be described a different way in this article. It previously read "which of&lt;br&gt;
the general rule and the exception table applies flips depending on the value," but &lt;strong&gt;the&lt;br&gt;
documentation names 6,144 as an exception to the general rule&lt;/strong&gt;, so applying the general rule&lt;br&gt;
there doesn't hold in the first place. Corrected before filing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both first replies came in on 2026-09-21.&lt;/strong&gt; The first stopped at confirming AWS's own measurement&lt;br&gt;
clears a third of the provisioned value; the second landed on a decision not to change the&lt;br&gt;
documentation. The underlying fact — a first-generation Single-AZ configuration at 2,048 MBps can&lt;br&gt;
lack a cache — is something the documentation genuinely never promised against, and not&lt;br&gt;
enumerating every corner case is a reasonable line to draw. &lt;strong&gt;What didn't go through, though, is&lt;br&gt;
separate from that: asking for the capacity to be published, and asking the management page's&lt;br&gt;
stated scope to cover first generation too, are both requests that a documented gap doesn't&lt;br&gt;
answer by itself.&lt;/strong&gt; Recording that they didn't land. The per-file-system way to check&lt;br&gt;
(&lt;code&gt;system node external-cache show&lt;/code&gt;) is documented, so &lt;strong&gt;checking it yourself is unaffected.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first one got a follow-up on 2026-09-24.&lt;/strong&gt; The "up to a third" figure turned out to be a&lt;br&gt;
sizing indicator rather than a hard limit — a write spends bandwidth twice over (once to the&lt;br&gt;
active server, once to sync the standby), and network burst capacity can push it above a third of&lt;br&gt;
the provisioned value, which AWS considers expected rather than anomalous. The table heading's&lt;br&gt;
"Maximum" is described as the ceiling to expect once burst isn't a factor and the file system is&lt;br&gt;
shared with other workloads. For sizing, AWS pointed back to the original rule of thumb: provision&lt;br&gt;
three times the required write throughput. &lt;strong&gt;This explanation doesn't dispute that the&lt;br&gt;
measurement exceeded the published value — it only says how to read that published value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The block protocol side's findings and filing status can be traced from the&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi"&gt;separate article&lt;/a&gt;.&lt;/strong&gt;&lt;br&gt;
Submission status across the three block-protocol articles and this article is collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-aws-feedback-status.md" rel="noopener noreferrer"&gt;AWS documentation notes and inquiries across the block-protocol articles&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What hasn't been measured yet
&lt;/h2&gt;

&lt;p&gt;Meaning not yet measured, not "can't be."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading more than 2x the cache size in one pass. At 512 GiB (2.15x the cache), 4.86x
(256.47 -&amp;gt; 1,246.67 MB/s). The low side saturating on SSD IOPS is confirmed. &lt;strong&gt;What was capping
the high side, around 1,250 MB/s, turned out to be transfer size and concurrency, not a
resource&lt;/strong&gt; (details under
a single-client figure that was actually the client's own limit
above)&lt;/li&gt;
&lt;li&gt;Effect of measurement duration. Measurement duration itself is not the cause. &lt;strong&gt;What carries
the gap is provisioned IOPS, payload compressibility, and unit-to-unit variance&lt;/strong&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md#%E6%B8%AC%E5%AE%9A%E6%99%82%E9%96%93%E3%81%A0%E3%81%91%E3%82%92%E6%8C%AF%E3%81%A3%E3%81%9F%E7%B5%90%E6%9E%9C--16-%E3%81%AE%E4%B8%8D%E5%9C%A82026-09-20" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)
(Japanese)&lt;/li&gt;
&lt;li&gt;Measuring FlexCache uncompressed on a 2048 MBps configuration. &lt;strong&gt;The "2.31x" could reverse
depending on configuration&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;What's capping the roughly 300 MB/s on the 128 MBps configuration. Reads on the same
configuration converge to 297-317 MB/s for both warm and cold (values from the same measurement
set as this article, not shown in the body table). This used to be described as "consistent
with plateauing on a burst," but reading burst remainders ruled that out. &lt;strong&gt;The remainder
plateaued at 99 -&amp;gt; 95% and returned to 99% after the load stopped — not exhausted.&lt;/strong&gt; Against a
disk burst ceiling of 600 MBps, the measured 254 MB/s is 42%; against a network burst ceiling of
1,250 MBps, roughly 285 MB/s is 23% — neither ceiling was reached. It isn't the roughly 590
MB/s single-flow ceiling either. Where it sits is unresolved
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification-status.md" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)
(Japanese). Note that &lt;code&gt;DiskReadBytes&lt;/code&gt; / &lt;code&gt;DataReadBytes&lt;/code&gt; in the same window was 87-97%, so
reads on this configuration are coming from disk — a contrast with the 2048 MBps
configuration's 1.4%, consistent with an in-memory cache of only 16 GB&lt;/li&gt;
&lt;li&gt;The host count at which Amazon S3's total stops being proportional to host count. It's still
proportional at 8 hosts / 38.3 Gbps&lt;/li&gt;
&lt;li&gt;Adding S3 Files proxies. A configuration with separate mounts running more &lt;code&gt;efs-proxy&lt;/code&gt;
instances hasn't been tested&lt;/li&gt;
&lt;li&gt;More than 8 hosts. The side sharing the same file dropped to a 1.12x increment at 8 hosts, so
where it stops hasn't been observed&lt;/li&gt;
&lt;li&gt;A point below 32 connections on non-overlapping ranges. It was already saturated at 32, so
the connection count where saturation begins hasn't been bracketed&lt;/li&gt;
&lt;li&gt;A sustained write longer than 900 seconds. No decay over 15 minutes, but nothing beyond that
has been checked&lt;/li&gt;
&lt;li&gt;Reads with the fraction served from cache varied. Capacity filled up and the run was
aborted, so none of the 7 planned points were captured&lt;/li&gt;
&lt;li&gt;Time-of-day effects. Two different days were checked, but not different times of day&lt;/li&gt;
&lt;li&gt;SMB's 7 scenarios under cache control. Stopped by fill, not capacity. &lt;code&gt;format=yes&lt;/code&gt; ignores
the run block's &lt;code&gt;xfersize&lt;/code&gt; and &lt;code&gt;threads&lt;/code&gt;, writing at 128 KiB and queue depth 2, so overwriting
the same file that otherwise reaches 1,488 MB/s only produced 165 MB/s (315 MB/s with
&lt;code&gt;formatxfersize=1m&lt;/code&gt;; queue depth stayed at 2). &lt;strong&gt;The parameter file points 8 SDs at a single
file; splitting each range into its own file looks likely to solve both the speed and the design
problem at once. This is a prediction, not a result.&lt;/strong&gt; Background in
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md#smb-%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E5%88%B6%E5%BE%A1%E4%B8%8B%E3%81%A7%E6%AD%A2%E3%81%BE%E3%81%A3%E3%81%9F-2-%E3%81%8B%E6%89%80" rel="noopener noreferrer"&gt;two places stuck under cache control&lt;/a&gt;
(Japanese)&lt;/li&gt;
&lt;li&gt;Why SMB's 300-second and 900-second values differ. Doesn't reproduce. Re-measuring at
6,144 MBps removes the window-length difference; the longer window's vdbench run had also been
interrupted, so it wasn't a comparable pair in the first place
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md#smb-%E3%81%AE%E7%AA%93%E9%95%B7-124-%E3%81%AE%E5%86%8D%E7%8F%BE%E4%B8%8D%E6%88%90%E7%AB%8B2026-09-20" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)
(Japanese)&lt;/li&gt;
&lt;li&gt;Conditions for raising Multichannel's channel count above 4. Setting
&lt;code&gt;max_connections_per_session&lt;/code&gt; to 32 still produced 4. &lt;strong&gt;The 4 comes from the client side's
&lt;code&gt;ConnectionCountPerRssNetworkInterface&lt;/code&gt;&lt;/strong&gt;, which determines channel count per RSS-capable NIC.
The server side's 32 is a per-session ceiling, not the limiter on the increasing side. Adding
NICs or raising this value hasn't been tested&lt;/li&gt;
&lt;li&gt;The discrepancy between server-side counters and the client-side measurement tool. The cause
was measurement method — aligning the window's start to a minute boundary and comparing only
the minutes fully contained in the window, &lt;strong&gt;the two systems agree within 0.2%&lt;/strong&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md#%E7%AA%93%E3%82%92%E5%88%86%E5%A2%83%E7%95%8C%E3%81%AB%E5%90%88%E3%82%8F%E3%81%9B%E3%81%9F-2-%E7%B3%BB%E7%B5%B1%E3%81%AE%E6%AF%94%E8%BC%832026-09-20" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)
(Japanese)&lt;/li&gt;
&lt;li&gt;What the 4 KiB random read ceiling actually is. It's on the client side. What matters is the
number of distinct IPs the client talks to and the client count, not thread count.
&lt;strong&gt;When citing this figure, write "per client"&lt;/strong&gt; — it isn't the file system's ceiling
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/throughput-iops-concurrency.md#4-kib-%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E8%AA%AD%E3%81%BF%E3%81%AE%E5%A4%A9%E4%BA%95%E3%81%AE%E6%89%80%E5%9C%A82026-09-20" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)
(Japanese)&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Why 4 KiB random reads plateau at 47.5% of provisioned SSD IOPS. The ratio doesn't reproduce.&lt;br&gt;
Smaller configurations are disk-IOPS-limited; larger ones follow a different formula.&lt;br&gt;
&lt;strong&gt;So "47.5% of provisioned" is a value specific to that one configuration, not an&lt;br&gt;
invariant with scale.&lt;/strong&gt; When citing it, always state the 6,144 MBps / 200,000 IOPS condition&lt;br&gt;
alongside it&lt;br&gt;
(&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md#%E6%8C%87%E5%AE%9A-iops-%E3%82%92-14-%E3%81%AB%E3%81%97%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E6%AF%94%E3%81%AE%E4%B8%8D%E6%88%90%E7%AB%8B2026-09-20" rel="noopener noreferrer"&gt;measurement&lt;/a&gt;)&lt;br&gt;
(Japanese)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Metadata-operation performance under a high file count (creating, enumerating, and stat-ing a&lt;br&gt;
large number of files). What this article measured is throughput, IOPS, and concurrency, not&lt;br&gt;
metadata operations when the file count is large. NetApp's High File Count NAS Workloads&lt;br&gt;
Technical Report suggests that inode capacity and a single directory's maximum size bear on&lt;br&gt;
enumeration cost, but there's no figure measured on this architecture (S3 API collect -&amp;gt; origin&lt;br&gt;
-&amp;gt; FlexCache distribution). Measuring it would need an origin volume holding a sufficient file&lt;br&gt;
count and a measurement setup for metadata operations over it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The time taken to enumerate a directory holding a large number of files (&lt;code&gt;ListObjectsV2&lt;/code&gt;, or an&lt;br&gt;
NFS / SMB directory walk). The enumeration cost when files concentrate in a single directory&lt;br&gt;
is something this project hasn't measured on this architecture&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FlexCache's metadata-replication cost under a high file count. How the cost of replicating&lt;br&gt;
metadata from origin to cache scales with file count is something this project hasn't measured&lt;br&gt;
on this architecture. It's a different axis from this article's throughput measurements&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Measuring three paths from the same host, the bottleneck sat in three different places for all&lt;br&gt;
three. A throughput capacity provisioned on the file system, client-side network bandwidth, and&lt;br&gt;
the CPU of a process on the client. &lt;strong&gt;This difference doesn't close as you add more clients.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And on the FSx for ONTAP side, there wasn't one ceiling.&lt;/strong&gt; With the same file system and the same&lt;br&gt;
connection count, the total swings 5.5x depending only on whether each EC2 client's read range overlaps.&lt;/p&gt;

&lt;p&gt;Which one to choose comes down to where you place the read side, which side holds the primary&lt;br&gt;
copy, and when you want a write visible on the bucket side. Throughput numbers are one input into&lt;br&gt;
that decision, and they come later in the order of things to check.&lt;/p&gt;

&lt;p&gt;What's worth taking away is a procedure: &lt;strong&gt;check the type of ceiling before comparing&lt;br&gt;
throughput.&lt;/strong&gt; Whether it's a provisioned value, client-side bandwidth, or a process's CPU on the&lt;br&gt;
client. Line up numbers of different types in the same column, and you get a table shaped like a&lt;br&gt;
comparison without being one.&lt;/p&gt;

&lt;p&gt;And a value measured with a single EC2 client can't determine that type. On the same file system,&lt;br&gt;
adding a second EC2 client grew the total 1.59x, and fixing transfer size and concurrency while&lt;br&gt;
staying at one client grew it 1.38x. Either way arrives at the same ceiling. &lt;strong&gt;There are two ways&lt;br&gt;
to check the type, and neither costs much extra.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A measurement that reproduces within 0.02-0.14% under identical conditions can still swing 30%&lt;br&gt;
from a single misunderstanding about conditions. "Stable" isn't "correct." And this article's&lt;br&gt;
figures are for one specific throughput capacity and one specific payload. &lt;strong&gt;They don't transfer&lt;br&gt;
directly as your environment's numbers. What transfers is the measurement approach.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hope this helps anyone running the same kind of measurement.&lt;/p&gt;

&lt;p&gt;Defaults to check before measuring and items to always record are collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/performance-testing-guide.md" rel="noopener noreferrer"&gt;Performance testing considerations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>performance</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Amazon FSx for NetApp ONTAP NVMe/TCP assumes RHEL 9.3: why multipathing fails on Amazon Linux 2023</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:42:42 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl</link>
      <guid>https://dev.to/aws-builders/amazon-fsx-for-netapp-ontap-nvmetcp-assumes-rhel-93-why-multipathing-fails-on-amazon-linux-2023-1afl</guid>
      <description>&lt;h1&gt;
  
  
  Amazon FSx for NetApp ONTAP's NVMe/TCP procedure assumes RHEL 9.3: why multipathing is unavailable on Amazon Linux 2023, and how to check before you are billed
&lt;/h1&gt;

&lt;p&gt;Followed the AWS procedure and run &lt;code&gt;cat /sys/module/nvme_core/parameters/multipath&lt;/code&gt; only to find&lt;br&gt;
the file isn't there?&lt;/p&gt;

&lt;p&gt;Moving your target Linux distribution to Amazon Linux 2023 can stop a documented command from&lt;br&gt;
working exactly as written. You are reading the procedure correctly, yet your environment alone&lt;br&gt;
gives a different result — and tracking the cause down to a kernel build setting takes time.&lt;br&gt;
This article is for anyone who got stuck at that point.&lt;/p&gt;

&lt;p&gt;What this article does and does not cover: it covers how to confirm multipathing is unusable on&lt;br&gt;
AL2023, and how to count whether it is actually using the second path. It does not cover&lt;br&gt;
performance comparisons or a generalized conclusion for distributions other than AL2023 (details&lt;br&gt;
under "What this article does not cover" below). All figures are from a Single-AZ configuration;&lt;br&gt;
Multi-AZ is unverified.&lt;/p&gt;

&lt;p&gt;dev.to series: &lt;code&gt;FSx for ONTAP Block Protocols, Measured&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What was checked
&lt;/h2&gt;

&lt;p&gt;Assuming no prior knowledge of FSx for ONTAP, here's what this article is confirming.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Location&lt;/strong&gt;: a single AWS VPC, single AZ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt;: one EC2 instance (Amazon Linux 2023)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target&lt;/strong&gt;: one Amazon FSx for NetApp ONTAP file system (second-generation SINGLE_AZ_2). The HA
pair's two controllers hold two paths to one namespace

&lt;ul&gt;
&lt;li&gt;Path 1: to the &lt;code&gt;optimized&lt;/code&gt; controller (shortest path)&lt;/li&gt;
&lt;li&gt;Path 2: to the &lt;code&gt;non-optimized&lt;/code&gt; controller. Only when multipathing is available can the host
tell that path 2 is a standby&lt;/li&gt;
&lt;/ul&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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fb6b83cd3d6bc516f12478138cc36bb30a3ae0ec3%2Fdocs%2F_assets%2Fimages%2Fs3burst-block-b-multipath-en%402x.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%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fb6b83cd3d6bc516f12478138cc36bb30a3ae0ec3%2Fdocs%2F_assets%2Fimages%2Fs3burst-block-b-multipath-en%402x.png" alt="One EC2 client connects to an FSx for ONTAP HA pair's two controllers, over Path 1 (optimized,&lt;br&gt;
the shortest route) and Path 2 (non-optimized, which looks like a normal route without ANA), both&lt;br&gt;
reaching the same one namespace" width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: an FSx for ONTAP HA pair has two paths to one namespace, and **Asymmetric Namespace&lt;br&gt;
Access is the mechanism that tells the host which one is the shortest path&lt;/em&gt;* (referred to as&lt;br&gt;
multipathing throughout this article). Without this mechanism in the kernel, those two paths&lt;br&gt;
don't appear as "one shortest path plus one standby": they appear as two separate devices&lt;br&gt;
pointing at the same data.*&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Role in this article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VPC / AZ&lt;/td&gt;
&lt;td&gt;Single AZ, so both paths to the HA pair's controllers sit on the same network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 client&lt;/td&gt;
&lt;td&gt;Checks for multipathing and the kernel configuration behind it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;optimized path&lt;/td&gt;
&lt;td&gt;The shortest path as determined by multipathing. Real data is expected to flow here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;non-optimized path&lt;/td&gt;
&lt;td&gt;The failover path. Without multipathing it appears as an ordinary path too&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AWS's&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/provision-nvme-linux.html" rel="noopener noreferrer"&gt;Provisioning NVMe/TCP for Linux&lt;/a&gt;&lt;br&gt;
includes a step that confirms multipath has come up: the fourth item under "To discover the target&lt;br&gt;
NVMe nodes". &lt;strong&gt;It says that a returned &lt;code&gt;Y&lt;/code&gt; means success&lt;/strong&gt; (wording as of 2026-09-17; every&lt;br&gt;
reference to that page below was checked on the same day).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/module/nvme_core/parameters/multipath
&lt;span class="c"&gt;# Y means success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On the Amazon Linux 2023 AMIs I used, that file did not exist.&lt;/strong&gt; The kernel does not have&lt;br&gt;
Asymmetric Namespace Access (a mechanism that tells the host which of several paths to the same&lt;br&gt;
namespace is the shortest one) in it. The range I checked is the three kernel series returned by&lt;br&gt;
&lt;code&gt;al2023-ami-kernel-default-x86_64&lt;/code&gt;, so I cannot write "AL2023 always behaves this way". Checking&lt;br&gt;
takes one command, and a later section gives it.&lt;/p&gt;

&lt;p&gt;The procedure is written for RHEL 9.3. The same page's "Before you begin" says&lt;br&gt;
&lt;code&gt;Create an EC2 instance running Red Hat Enterprise Linux (RHEL) 9.3&lt;/code&gt;. Immediately after that there&lt;br&gt;
is a statement about other distributions. In summary: on AMIs other than RHEL 9.3 some of the&lt;br&gt;
utilities may already be present or the install command may differ, but &lt;strong&gt;apart from installing&lt;br&gt;
packages, the commands in this section are valid on other EC2 Linux AMIs too&lt;/strong&gt; (the original wording&lt;br&gt;
is at the end of "Before you begin" on that page).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this article reports is two counterexamples to that statement.&lt;/strong&gt; Neither of them is&lt;br&gt;
explained by package installation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command from the documentation&lt;/th&gt;
&lt;th&gt;Result on AL2023&lt;/th&gt;
&lt;th&gt;A packaging problem?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat /etc/nvme/hostnqn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;File does not exist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No. &lt;code&gt;nvme-cli&lt;/code&gt; is installed; whether it creates this file differs by distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat /sys/module/nvme_core/parameters/multipath&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;File does not exist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No. This is a &lt;strong&gt;kernel build option&lt;/strong&gt; (&lt;code&gt;CONFIG_NVME_MULTIPATH&lt;/code&gt;) and a package cannot change it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;You can check this before any billing starts.&lt;/strong&gt; That is the most practical part of this article,&lt;br&gt;
I think.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not a claim that "the documentation is wrong". It is a report that &lt;strong&gt;these two do not&lt;br&gt;
fall inside&lt;/strong&gt; the stated range (apart from installing packages, valid on other AMIs too).&lt;br&gt;
The state of my feedback to AWS is at the end of this article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, after bringing in a kernel that does have multipathing, I found that &lt;strong&gt;the &lt;code&gt;optimized&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;non-optimized&lt;/code&gt; output of &lt;code&gt;nvme list-subsys&lt;/code&gt; says "there are two paths", not "two paths are in&lt;br&gt;
use".&lt;/strong&gt; Whether they are in use has to be counted separately, and there is a trap in choosing&lt;br&gt;
which table to count.&lt;/p&gt;

&lt;p&gt;What this article does not cover. It does not compare performance. The figures here exist to&lt;br&gt;
show that two clients agreed under the same conditions, and &lt;strong&gt;they cannot be used for sizing.&lt;/strong&gt; What&lt;br&gt;
moves the numbers is in &lt;a href="https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi"&gt;a separate article&lt;/a&gt;. All figures are from a&lt;br&gt;
Single-AZ configuration; Multi-AZ is unverified.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is one of three, but there is no reading order. The block-protocol measurements are split&lt;br&gt;
by reader.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/aws-builders/moving-on-premises-block-storage-to-aws-the-divide-by-625-mbps-sizing-approach-comes-out-wrong-31pc"&gt;Session and queue counts&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Anyone about to mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Checking whether multipathing is available&lt;/strong&gt; (this article)&lt;/td&gt;
&lt;td&gt;Anyone who followed the procedure and got stuck&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi"&gt;What moves the numbers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Anyone measuring or citing figures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of them assumes the other two. Read only the part you need.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The environment I measured
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File system&lt;/td&gt;
&lt;td&gt;Amazon FSx for NetApp ONTAP, second generation SINGLE_AZ_2, 6,144 MBps × 1 HA pair, 4,096 GiB SSD, 200,000 provisioned IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1, single AZ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONTAP&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;9.18.1&lt;/strong&gt; (the reads and writes in the agreement table). &lt;strong&gt;For the 12 sequential-write points I failed to record the version.&lt;/strong&gt; &lt;code&gt;FileSystemTypeVersion&lt;/code&gt; returned &lt;code&gt;null&lt;/code&gt; and I tore the environment down before re-reading it from the cluster API. The kernel investigation also spans deployments on other versions. The cross-article version mapping is in the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-ontap-version-matrix.md" rel="noopener noreferrer"&gt;ONTAP version matrix&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVMe read cache&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Disabled&lt;/strong&gt; (confirmed on both nodes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LUN / namespace&lt;/td&gt;
&lt;td&gt;600 GiB, on a 900 GiB volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before measuring&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Wrote the full 600 GiB once.&lt;/strong&gt; Unwritten blocks on a thin-provisioned volume return zeros, so reading without writing first measures nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;c5n.9xlarge; 50 Gbps network is a guaranteed figure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instrument&lt;/td&gt;
&lt;td&gt;VDBENCH 5.04.07 directly, &lt;strong&gt;&lt;code&gt;iorate=max&lt;/code&gt; (unlimited)&lt;/strong&gt;, 512 threads, 60 s warm-up + 300 s measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test data / efficiency settings&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not recorded whether the VDBENCH payload was compressible, or whether volume inline efficiency (&lt;code&gt;StorageEfficiencyEnabled&lt;/code&gt;) was enabled.&lt;/strong&gt; On the file-protocol side, efficiency settings measurably affected figures; whether the same applies to block is unconfirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The window is 300 seconds, so the figures in this article include burst. They cannot be cited as a&lt;br&gt;
baseline.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The figures in this article are &lt;code&gt;iorate=max&lt;/code&gt; saturation points, not operating points. No target&lt;br&gt;
IOPS was given; they are what the system took when everything available was pushed at it. On the&lt;br&gt;
file-protocol side of the same repository, giving a target of 4,400 IOPS produced 4,406 MB/s at&lt;br&gt;
8.86 ms, while unlimited produced 4,204 MB/s at 121.79 ms. &lt;strong&gt;Unlimited was 5% lower in&lt;br&gt;
throughput and 14 times higher in response time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With 512 threads fixed and no rate limit, response time becomes the identity &lt;code&gt;512 ÷ IOPS&lt;/code&gt;&lt;br&gt;
(derived 216.6 ms against measured 216.588 ms in a separate run). The figures below can be read&lt;br&gt;
through that identity: 1,277.48 MB/s is about 401 ms, and 131,736.5 IOPS is about&lt;br&gt;
3.9 ms. &lt;strong&gt;Both are derived values and were not recorded as measurements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For this article's purpose, being a saturation point is not a disadvantage. The figures are&lt;br&gt;
here to show that Rocky and RHEL agreed under the same conditions, and as long as both are&lt;br&gt;
saturated the same way, the agreement test holds. &lt;strong&gt;Those values still cannot be used for&lt;br&gt;
sizing.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Behaviour on this AMI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;On this AMI the file named at the top does not exist.&lt;/strong&gt; Reading the kernel configuration gives&lt;br&gt;
&lt;code&gt;# CONFIG_NVME_MULTIPATH is not set&lt;/code&gt;, and &lt;code&gt;modinfo nvme_core&lt;/code&gt; has no multipath parameter either.&lt;br&gt;
So multipathing is not in the kernel.&lt;/p&gt;

&lt;p&gt;As a result the two controllers are not merged into one device; they appear as &lt;strong&gt;separate devices&lt;br&gt;
pointing at the same namespace UUID&lt;/strong&gt; (&lt;code&gt;/dev/nvme2n1&lt;/code&gt; and &lt;code&gt;/dev/nvme3n1&lt;/code&gt;). &lt;code&gt;nvme list-subsys&lt;/code&gt; shows&lt;br&gt;
no &lt;code&gt;optimized&lt;/code&gt; / &lt;code&gt;non-optimized&lt;/code&gt; either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AWS procedure is written for RHEL 9.3, where it is present.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please do not generalise this to "multipathing is not available on AL2023". What I observed were the&lt;br&gt;
AMIs that the &lt;code&gt;al2023-ami-kernel-default-x86_64&lt;/code&gt; SSM parameter returned on 2026-09-12 and&lt;br&gt;
09-13, and &lt;strong&gt;I did not record the kernel versions.&lt;/strong&gt; The template said "record the kernel version&lt;br&gt;
at measurement time" in place of pinning the AMI, and the block phase did not implement that.&lt;br&gt;
That is a gap in my records.&lt;/p&gt;

&lt;p&gt;Checking in your own environment is more reliable, and it is one command.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; NVME_MULTIPATH /boot/config-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;I later widened the observation to three kernel series.&lt;/strong&gt; All three returned by&lt;br&gt;
&lt;code&gt;al2023-ami-kernel-default-x86_64&lt;/code&gt; (6.1.186 / 6.12.103 / 6.18.48) gave&lt;br&gt;
&lt;code&gt;# CONFIG_NVME_MULTIPATH is not set&lt;/code&gt;. You can check this before creating a file system, by&lt;br&gt;
extracting the config from the kernel package in the repository. It is known before billing&lt;br&gt;
starts, so checking in that order is cheaper.&lt;/p&gt;

&lt;p&gt;To measure multipathing I then brought in another distribution. &lt;strong&gt;The AWS procedure is written for RHEL 9.3,&lt;br&gt;
so I put both Rocky Linux 9.7, a rebuild, and RHEL 9.7 itself on the same namespace and compared&lt;br&gt;
them.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;RHEL 9.7&lt;/th&gt;
&lt;th&gt;Rocky 9.7&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB sequential read&lt;/td&gt;
&lt;td&gt;1,277.48 MB/s&lt;/td&gt;
&lt;td&gt;1,276.60–1,279.22 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random read&lt;/td&gt;
&lt;td&gt;131,736.5 IOPS&lt;/td&gt;
&lt;td&gt;131,550.6 / 131,749.4 IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random write&lt;/td&gt;
&lt;td&gt;195,439.8 IOPS&lt;/td&gt;
&lt;td&gt;194,960.6 IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Only the reads agreed in the same state&lt;/strong&gt; (within 0.1%). Same minor version, same &lt;code&gt;nvme-cli&lt;/code&gt;&lt;br&gt;
(2.16-1.el9), same cold state. The kernels differ in z-stream (5.14.0-611.55.1 against 611.5.1).&lt;br&gt;
For reads, there is now one piece of grounds for reading a rebuild's figure as the figure for the&lt;br&gt;
product itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Both write workloads had the Rocky side measured warm, so the states are not aligned. Counting&lt;br&gt;
the 0.3% difference on 4 KiB random write as "agreement" was my error, and &lt;strong&gt;the note in that same&lt;br&gt;
table contradicted my own conclusion&lt;/strong&gt; (corrected 2026-09-20). For that shape I can say neither&lt;br&gt;
that it holds nor that it does not.&lt;/p&gt;

&lt;p&gt;Sequential write was re-measured later, in the same state, for that shape alone. &lt;strong&gt;It does not&lt;br&gt;
agree (2026-09-17).&lt;/strong&gt; On a different file system (second generation, 1,536 MBps, 50,000 provisioned&lt;br&gt;
IOPS), same namespace, same state, same parameters, six runs each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;th&gt;Spread&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rocky 9.7&lt;/td&gt;
&lt;td&gt;842.70&lt;/td&gt;
&lt;td&gt;1,040.26&lt;/td&gt;
&lt;td&gt;1,092.50&lt;/td&gt;
&lt;td&gt;29.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RHEL 9.7&lt;/td&gt;
&lt;td&gt;1,105.75&lt;/td&gt;
&lt;td&gt;1,112.65&lt;/td&gt;
&lt;td&gt;1,129.30&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Units MB/s (1 MiB sequential write, 8 threads, &lt;code&gt;o_direct&lt;/code&gt;, 120-second window). These 12&lt;br&gt;
points were taken under conditions that differ from the environment described at the top: a&lt;br&gt;
different file system (second generation, 1,536 MBps, 50,000 provisioned IOPS), 8 threads rather&lt;br&gt;
than 512, and 120 seconds rather than 300. The &lt;code&gt;512 ÷ IOPS&lt;/code&gt; reading given at the top cannot be&lt;br&gt;
applied to these 12 points. &lt;strong&gt;RHEL's minimum is above Rocky's maximum&lt;/strong&gt;: six against six, with no&lt;br&gt;
overlap in range. RHEL's median is 7.0% above Rocky's, and the asymmetry in spread is larger than&lt;br&gt;
the difference in medians.&lt;/p&gt;

&lt;p&gt;So "a rebuild's figure can be read as the figure for the product itself" is limited to the&lt;br&gt;
reads. When citing a sequential-write figure, say which of the two it was measured on.&lt;br&gt;
4 KiB random write was not re-measured in the same state, so for that shape I can say neither&lt;br&gt;
that it holds nor that it does not. &lt;strong&gt;I cannot state a cause.&lt;/strong&gt; The only difference on record is&lt;br&gt;
the kernel z-stream, and I did not vary it on its own. During the runs the file system side was at&lt;br&gt;
72–81% disk throughput, 16–19% IOPS and 31–38% network, none of them saturated (so this is not a&lt;br&gt;
case of the two meeting at a ceiling). For these 12 points I failed to record the ONTAP version.&lt;br&gt;
&lt;code&gt;FileSystemTypeVersion&lt;/code&gt; returned &lt;code&gt;null&lt;/code&gt; and I tore the environment down before re-reading it from&lt;br&gt;
the cluster API.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;iopolicy&lt;/code&gt; on a kernel with multipathing, and the value the procedure states
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This is the third counterexample.&lt;/strong&gt; &lt;code&gt;iopolicy&lt;/code&gt; (Linux's multipath setting that decides how I/O&lt;br&gt;
is distributed across multiple paths to an NVMe device: &lt;code&gt;round-robin&lt;/code&gt; cycles through them,&lt;br&gt;
&lt;code&gt;queue-depth&lt;/code&gt; sends to whichever has the shortest queue) is what step 5 of the same procedure&lt;br&gt;
says to confirm as &lt;code&gt;round-robin&lt;/code&gt; (and the example &lt;code&gt;nvme list-subsys&lt;/code&gt; output at step 7 also shows&lt;br&gt;
&lt;code&gt;iopolicy=round-robin&lt;/code&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Value the documentation says to confirm&lt;/td&gt;
&lt;td&gt;&lt;code&gt;round-robin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Measured on Rocky Linux 9.7 / RHEL 9.7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;queue-depth&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What set it&lt;/td&gt;
&lt;td&gt;the udev rule &lt;code&gt;71-nvmf-netapp.rules&lt;/code&gt; shipped by &lt;code&gt;nvme-cli&lt;/code&gt; (2.16-1.el9)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The kernel default is &lt;code&gt;numa&lt;/code&gt;, neither &lt;code&gt;round-robin&lt;/code&gt; nor &lt;code&gt;queue-depth&lt;/code&gt;.&lt;/strong&gt; It was &lt;code&gt;queue-depth&lt;/code&gt; in&lt;br&gt;
this environment because the NetApp-oriented udev rule bundled with the &lt;code&gt;nvme-cli&lt;/code&gt; package set it.&lt;br&gt;
Following the confirmation step returns a value other than the expected one.&lt;/p&gt;

&lt;p&gt;I measured the effect on performance. It is negligible: switching policy mid-run produced no&lt;br&gt;
step change, and the difference was 0.5%. The figures and the method are in&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/why-amazon-fsx-for-netapp-ontap-block-performance-differed-by-about-2x-on-the-same-specified-35pi"&gt;a separate article&lt;/a&gt; (C). So this is not a story about &lt;code&gt;round-robin&lt;/code&gt; being&lt;br&gt;
required for speed. &lt;strong&gt;The only part worth reporting is that the confirmation step returns a value&lt;br&gt;
that differs from the documentation.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This can change with the &lt;code&gt;nvme-cli&lt;/code&gt; version. What I observed here is one version, 2.16-1.el9,&lt;br&gt;
and &lt;strong&gt;I did not confirm whether other versions ship the same udev rule.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  How to check whether multipathing is using the second path
&lt;/h2&gt;

&lt;p&gt;This section was added afterwards. It became necessary in order to do the analysis above, and it&lt;br&gt;
is also the reason I once published a wrong conclusion.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nvme list-subsys&lt;/code&gt; shows the two controllers as &lt;code&gt;optimized&lt;/code&gt; / &lt;code&gt;non-optimized&lt;/code&gt;, and prints &lt;code&gt;iopolicy&lt;/code&gt;&lt;br&gt;
as well. &lt;strong&gt;That is a display of "there are two paths", not of "two paths are in use".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first I varied &lt;code&gt;iopolicy&lt;/code&gt; three ways, compared total throughput, saw that only &lt;code&gt;queue-depth&lt;/code&gt; was&lt;br&gt;
faster, and wrote that "only &lt;code&gt;queue-depth&lt;/code&gt; uses the second path". &lt;strong&gt;That was wrong.&lt;/strong&gt; I had not&lt;br&gt;
counted bytes per path. A client-side total that does not increase is consistent with two different&lt;br&gt;
facts: "the second path is not being used", and "it is being used but the ceiling is nearer than&lt;br&gt;
the path".&lt;/p&gt;
&lt;h3&gt;
  
  
  Bytes per path can be read from two places
&lt;/h3&gt;

&lt;p&gt;On the ONTAP side they can be read from a counter table. &lt;strong&gt;There is a trap here.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Result in this configuration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvmf_lif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Zero rows.&lt;/strong&gt; Still empty after writing 600 GiB over NVMe/TCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reports &lt;strong&gt;0 bytes&lt;/strong&gt; for the LIF in question. It does not count NVMe-oF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;nvmf_tcp_port&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Has the real data.&lt;/strong&gt; &lt;code&gt;read_data&lt;/code&gt; / &lt;code&gt;write_data&lt;/code&gt; / &lt;code&gt;total_ops&lt;/code&gt; per LIF&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"The table exists but has no rows" and "this version does not have that table" add up to the same&lt;br&gt;
appearance. Return 0 and move on, and &lt;strong&gt;you reach the correct conclusion, "the second path saw&lt;br&gt;
0 bytes", for the wrong reason.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It can also be counted on the client side. NVMe/TCP controllers have different destination&lt;br&gt;
addresses, so aggregating established sockets by destination separates the paths. That is a second&lt;br&gt;
source, independent of the ONTAP side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tin&lt;/span&gt; state established | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/:4420/{...}'&lt;/span&gt;   &lt;span class="c"&gt;# sum bytes_sent / bytes_received per destination&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Counted properly, the second path was not running
&lt;/h3&gt;

&lt;p&gt;I ran four workloads under &lt;code&gt;queue-depth&lt;/code&gt; and took before/after differences.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Read bytes&lt;/th&gt;
&lt;th&gt;Write bytes&lt;/th&gt;
&lt;th&gt;Total ops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;optimized&lt;/td&gt;
&lt;td&gt;+about 1,012 GiB&lt;/td&gt;
&lt;td&gt;+about 730 GiB&lt;/td&gt;
&lt;td&gt;641,908 → 120,008,866&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;non-optimized&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;±0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;±0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 → 4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Neither reads nor writes went to the second path.&lt;/strong&gt; The ONTAP side and the client side agreed, and&lt;br&gt;
it was the same on Rocky and on RHEL (the second path's increment across all workloads was about&lt;br&gt;
20 KB).&lt;/p&gt;

&lt;p&gt;Further, switching &lt;code&gt;iopolicy&lt;/code&gt; within a single run produces no step change. With a sequential read&lt;br&gt;
running I changed &lt;code&gt;queue-depth&lt;/code&gt; → &lt;code&gt;numa&lt;/code&gt; → &lt;code&gt;queue-depth&lt;/code&gt;, and the values sampled at one-second&lt;br&gt;
intervals stayed inside the same 1,150–1,400 MB/s range both before and after each switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All three kernel series returned by &lt;code&gt;al2023-ami-kernel-default-x86_64&lt;/code&gt; (6.1.186 / 6.12.103 /
6.18.48) had &lt;code&gt;CONFIG_NVME_MULTIPATH&lt;/code&gt; unset.&lt;/strong&gt; Measuring multipathing requires a different distribution.
&lt;strong&gt;Asking AWS why AL2023 is excluded from the procedure (reply 2026-09-21) surfaced a likely
reason:&lt;/strong&gt; &lt;code&gt;nvme connect-all&lt;/code&gt; establishes both paths and auto-discovers the alternate LIF even
when only one is given, but without &lt;code&gt;CONFIG_NVME_MULTIPATH&lt;/code&gt; the kernel never merges them into
one namespace device. The procedure's later steps assume that merge already happened, so
proceeding unmerged leaves the other path writable against the same namespace with no multipath
software mediating it: &lt;strong&gt;a data-corruption risk&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can check before creating a file system.&lt;/strong&gt; Extract the config from the kernel package in the
repository and you know before billing starts. On a running instance it is the one command
&lt;code&gt;grep -i NVME_MULTIPATH /boot/config-$(uname -r)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rocky Linux 9.7's figures can be read as RHEL 9.7's figures, but only for the reads.&lt;/strong&gt; On the
same namespace in the same state they agreed within 0.1%. Both write workloads had the Rocky side
measured warm, so the states are not aligned. Counting the 0.3% difference on 4 KiB random write
as agreement was my error, and for that shape I can say neither that it holds nor that it does
not (corrected 2026-09-20). Sequential write does not agree: re-measured later in the same state,
six runs each, RHEL's median was 7.0% higher and RHEL's minimum was above Rocky's maximum (spread
29.6% for Rocky against 2.1% for RHEL). When citing, say which of the two it was measured on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;optimized&lt;/code&gt; / &lt;code&gt;non-optimized&lt;/code&gt; in &lt;code&gt;nvme list-subsys&lt;/code&gt; is a display of path existence.&lt;/strong&gt; For whether
they are in use, count with the &lt;code&gt;nvmf_tcp_port&lt;/code&gt; counters. &lt;code&gt;nvmf_lif&lt;/code&gt; returns no rows, and &lt;code&gt;lif&lt;/code&gt;
does not count NVMe-oF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this configuration, not one byte went to the second path even under &lt;code&gt;queue-depth&lt;/code&gt;.&lt;/strong&gt; Two
places agreed (the ONTAP-side counters and the client-side sockets), and it was the same on Rocky
and on RHEL&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AWS documentation notes and support inquiries
&lt;/h2&gt;

&lt;p&gt;This article touches three places in the documentation. All three were submitted on 2026-09-17.&lt;/p&gt;

&lt;p&gt;The source for all of them is&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/provision-nvme-linux.html" rel="noopener noreferrer"&gt;Provisioning NVMe/TCP for Linux&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Remark&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Against "apart from installing packages, valid on other EC2 Linux AMIs too": &lt;code&gt;cat /etc/nvme/hostnqn&lt;/code&gt; finds no file on AL2023&lt;/td&gt;
&lt;td&gt;Submitted (2026-09-17). &lt;strong&gt;Reproduced in the same environment, confirming the wording doesn't hold as-is for the AL2023 environment checked.&lt;/strong&gt; The two proposed fixes (scoping the wording, adding a generation step) have been shared as an improvement request. Adoption, content, and timing are unconfirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Against the same statement: &lt;code&gt;cat /sys/module/nvme_core/parameters/multipath&lt;/code&gt; finds no file on AL2023 (kernel build option)&lt;/td&gt;
&lt;td&gt;Submitted (2026-09-17). &lt;strong&gt;The observation was confirmed and filed as documentation feedback. Fix timing undecided.&lt;/strong&gt; A follow-up question ("why is AL2023 excluded from scope?") also got a technical explanation: proceeding with the paths unmerged leaves the other path writable against the same namespace with no multipath software mediating it, a &lt;strong&gt;data-corruption risk&lt;/strong&gt;, so the exclusion has a technical rationale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step 5 directs confirming that &lt;code&gt;iopolicy&lt;/code&gt; is &lt;code&gt;round-robin&lt;/code&gt;, but the measurement on Rocky 9.7 / RHEL 9.7 is &lt;code&gt;queue-depth&lt;/code&gt;. &lt;code&gt;nvme-cli&lt;/code&gt;'s version boundary is &lt;strong&gt;2.11 -&amp;gt; 2.12&lt;/strong&gt; (see below)&lt;/td&gt;
&lt;td&gt;Submitted (2026-09-17). &lt;strong&gt;On 2026-09-30 it was acknowledged that a note is needed stating whether &lt;code&gt;iopolicy&lt;/code&gt; resolves to &lt;code&gt;round-robin&lt;/code&gt; or &lt;code&gt;queue-depth&lt;/code&gt; depends on the environment, and this was fed to the responsible team&lt;/strong&gt; (with a note that NetApp's own published documentation carries the same point). The content and timing of any change are not disclosed in advance, so they are unconfirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The form I submitted was this. Verbatim quotation (which line of which step), reproduction steps,&lt;br&gt;
expected and measured values, and the range I confirmed across three kernel series, written as&lt;br&gt;
such. &lt;strong&gt;Not generalising to "multipathing is not available on AL2023"&lt;/strong&gt; is the same in the feedback: the&lt;br&gt;
observation is limited to the three series returned by &lt;code&gt;al2023-ami-kernel-default-x86_64&lt;/code&gt;.&lt;br&gt;
For the second one I also included the control under which the procedure does hold (on a kernel&lt;br&gt;
with &lt;code&gt;CONFIG_NVME_MULTIPATH=y&lt;/code&gt;, &lt;code&gt;Y&lt;/code&gt; is returned). It is not a report that the procedure itself is&lt;br&gt;
wrong.&lt;/p&gt;

&lt;p&gt;The third is not submitted as a performance problem. &lt;strong&gt;The difference between policies is 0.5% in&lt;br&gt;
this configuration, and the report is the single point that the confirmation step returns a value&lt;br&gt;
differing from the documentation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The third changed shape before submission, after I looked at the package side. Reading&lt;br&gt;
&lt;code&gt;71-nvmf-netapp.rules&lt;/code&gt; out of four builds of the public &lt;code&gt;nvme-cli&lt;/code&gt; package (the SRPMs for 2.11 /&lt;br&gt;
2.13 / 2.16, plus the installed 2.16-1.el9) showed that 2.11 has &lt;code&gt;round-robin&lt;/code&gt; and 2.13 changed it&lt;br&gt;
to &lt;code&gt;queue-depth&lt;/code&gt; (confirmed 2026-09-17). At that point 2.12 had not been examined, so the boundary&lt;br&gt;
could only be written as "somewhere between 2.11 and 2.13".&lt;/p&gt;

&lt;p&gt;Submission status across the three block-protocol articles and S3 Burst Part 2 is collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-aws-feedback-status.md" rel="noopener noreferrer"&gt;AWS documentation notes and inquiries across the block-protocol articles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A pointer to the upstream v2.11 and v2.12 sources followed, showing the boundary is v2.12.&lt;br&gt;
Fetching and checking that commit (2026-09-19, &lt;code&gt;71-nvmf-netapp.rules.in&lt;/code&gt;) confirmed that&lt;br&gt;
&lt;code&gt;queue-depth&lt;/code&gt; was already in place as of v2.12. &lt;strong&gt;So the version boundary is settled at&lt;br&gt;
2.11 -&amp;gt; 2.12&lt;/strong&gt; (the original "somewhere between 2.11 and 2.13" narrowed to 2.12, from both that&lt;br&gt;
pointer and my own re-check). This is agreed to be a version drift rather than an error in the&lt;br&gt;
documentation.&lt;/p&gt;

&lt;p&gt;On 2026-09-30, it was acknowledged for this point that a note is needed stating that whether&lt;br&gt;
&lt;code&gt;iopolicy&lt;/code&gt; resolves to &lt;code&gt;round-robin&lt;/code&gt; or &lt;code&gt;queue-depth&lt;/code&gt; depends on the environment, and it was fed to&lt;br&gt;
the responsible team.&lt;br&gt;
&lt;a href="https://docs.netapp.com/us-en/ontap-sanhost/nvme-rhel-9x.html" rel="noopener noreferrer"&gt;NetApp's NVMe-oF procedure for RHEL 9.x&lt;/a&gt;&lt;br&gt;
carries the same point (from RHEL 9.7 the default is &lt;code&gt;queue-depth&lt;/code&gt;; on 9.6 &lt;code&gt;round-robin&lt;/code&gt; is the&lt;br&gt;
default with &lt;code&gt;queue-depth&lt;/code&gt; selectable), consistent with this understanding. &lt;strong&gt;The content and timing&lt;br&gt;
of any change are not disclosed in advance, so its reflection in the documentation is unconfirmed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary record of the figures
&lt;/h2&gt;

&lt;p&gt;The primary record of the figures and their conditions is in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/perf-matrix-results.md" rel="noopener noreferrer"&gt;the protocol measurement results&lt;/a&gt;,&lt;br&gt;
and the reproduction steps are in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/ja/verification/block-measurement-runbook.md" rel="noopener noreferrer"&gt;the block measurement runbook&lt;/a&gt;.&lt;br&gt;
What is unconfirmed is written as unconfirmed there, so &lt;strong&gt;please do not drop that when citing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How to check whether multipathing is available, counter-table traps, and &lt;code&gt;iopolicy&lt;/code&gt; version differences are&lt;br&gt;
collected in&lt;br&gt;
&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/b39f5d8a7bcd3365d9e84bf64ec12ff8188678a1/docs/en/reference/block-protocol-testing-guide.md" rel="noopener noreferrer"&gt;Considerations when measuring FSx for ONTAP's block protocols&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>linux</category>
      <category>networking</category>
      <category>cloud</category>
    </item>
    <item>
      <title>AWS Transform Now Supports Block Storage Migration to FSx for ONTAP — Benefits and Pitfalls from a Hands-On EC2 Test</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:42:13 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-transform-now-supports-block-storage-migration-to-fsx-for-ontap-benefits-and-pitfalls-from-a-1hhe</link>
      <guid>https://dev.to/aws-builders/aws-transform-now-supports-block-storage-migration-to-fsx-for-ontap-benefits-and-pitfalls-from-a-1hhe</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is Part 2 of the series "The Data Foundation for AWS Modernization." &lt;a href="https://dev.to/aws-builders/designing-aws-modernization-with-vmware-migration-as-the-entry-point-why-fsx-for-ontap-as-the-3k24"&gt;Part 1&lt;/a&gt; set out the lens: migration is not the goal but the entry point, and putting FSx for ONTAP at the data foundation lets you re-choose the compute. This part takes that entry point through AWS Transform, the core of modernization, and verifies hands-on that the data area lands on FSx for ONTAP as part of the migration on AWS. Migration from an on-premises VMware environment will come separately; the idea here is to start modernizing from the tightly coupled EC2 × EBS configuration by decoupling the data store.&lt;/p&gt;

&lt;p&gt;Until now, when you migrated servers with AWS Transform (MGN), every disk was placed on Amazon EBS. If the source was not ONTAP and you wanted the data area on Amazon FSx for NetApp ONTAP (FSx for ONTAP below), you needed a "two-step migration" — migrate to EBS first, then move the data — or a third-party block storage migration tool.&lt;/p&gt;

&lt;p&gt;With the update of 30 August 2026, &lt;strong&gt;FSx for ONTAP can now be chosen directly as the MGN target storage type&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Transform announces general availability of Amazon FSx for NetApp ONTAP support: &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-transform-fsx-netapp-ontap-support/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/09/aws-transform-fsx-netapp-ontap-support/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That looked useful, so I tried it hands-on straight away.&lt;br&gt;
The source in this test was &lt;strong&gt;not&lt;/strong&gt; an on-premises VMware environment but an EC2 instance already on AWS (Amazon Linux 2023). Using that EC2 as the source, I ran the new path that migrates the data area (EBS) directly to an FSx for ONTAP iSCSI LUN.&lt;/p&gt;

&lt;p&gt;Running it for real turned up several pitfalls that reading the documentation alone does not reveal.&lt;br&gt;
Besides sorting out what this new feature can and cannot do, this article records the traps I actually stepped on during the test — errors hidden behind a successful job, and an unexpected capacity spike.&lt;/p&gt;

&lt;p&gt;Here are the highlights up front, especially the traps to watch for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only &lt;strong&gt;data volumes&lt;/strong&gt; can be targeted. Boot is always Amazon EBS.&lt;/li&gt;
&lt;li&gt;What became GA is "the MGN target storage type". It is a different thing from presenting FSx for ONTAP as a VMware datastore (the Amazon EVS side).&lt;/li&gt;
&lt;li&gt;In MGN's phases, "SNAPSHOT" was a volume Snapshot done as a metadata operation, and "LAUNCH" was the creation of a FlexClone (about 44 seconds for 8 GiB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Trap 1] Finalize is not just clean-up; it is the step where physical capacity temporarily peaks (about 2x)&lt;/strong&gt;. That 2x, though, is a measured peak, not a sizing guideline: the official guidance is to provision &lt;strong&gt;3x the migration data&lt;/strong&gt; as SSD capacity (see below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Trap 2]&lt;/strong&gt; Even when the job status becomes &lt;code&gt;COMPLETED&lt;/code&gt;, &lt;strong&gt;a failed final snapshot (fallback) or the creation of an unbootable target can be hiding behind it&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Test Assumptions and Scope
&lt;/h3&gt;

&lt;p&gt;The test was run with the following environment and scope.&lt;br&gt;
(Region: ap-northeast-1 / Source: Amazon EC2 (AL2023) / ONTAP: 9.18.1P3D1)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[What was run]&lt;/strong&gt;&lt;br&gt;
The whole flow: agent installation → replication → test launch → cutover → Finalize → teardown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[Out of scope for this article (not verified)]&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migration with VMware as the source (no vCenter was prepared, so EC2 stood in as the source)&lt;/li&gt;
&lt;li&gt;FSx for ONTAP as an Amazon EVS datastore (a different feature)&lt;/li&gt;
&lt;li&gt;Durations at production-scale data volumes (the test used a tiny 16 GiB environment, dominated by fixed overhead)&lt;/li&gt;
&lt;li&gt;Windows sources, and access over SMB / NFS&lt;/li&gt;
&lt;li&gt;Post-migration optimization with &lt;code&gt;lun move start&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The Shape of the Configuration
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo204zcvvabvj4phoc428.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo204zcvvabvj4phoc428.png" alt="A table-shaped diagram pairing three Amazon EC2 instances with their boot / root volume and their data volume. The rows are, from the top, the source (Amazon EC2 in this test), staging and after cutover. The columns are the boot / root volume and the data volume. The boot column is Amazon Elastic Block Store in all three rows: boot 8 GiB at the source, the boot staging at staging, and root 8 GiB after cutover. In the data column, only the source row is Amazon Elastic Block Store (data 4 GiB × 2); staging and after cutover are Amazon FSx for NetApp ONTAP (data staging, then the target FlexVol over iSCSI). The rows are joined downwards by block replication and cutover arrows" width="800" height="793"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: three EC2 instances and where each volume goes: the boot column is EBS in all three rows, and only the data column changes to FSx for ONTAP from staging on (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/VMware-Migration-EC2-ONTAP/3421f0b8d162eff3f612f544acdf99f624411a0d/docs/_assets/images/png/atx-fsxn-data-path-en-dark%402x.png" rel="noopener noreferrer"&gt;dark theme&lt;/a&gt;)&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9inulvx8bxc11ekqat6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9inulvx8bxc11ekqat6.png" alt="The control path. AWS Transform reaches the ONTAP management endpoint (REST API / 443) inside the customer VPC through AWS PrivateLink and a Network Load Balancer. The management endpoint belongs to Amazon FSx for NetApp ONTAP, whose icon and service name sit directly above the box. The client certificate goes from AWS Secrets Manager, outside the VPC, to AWS Transform" width="800" height="427"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: the control path, from AWS Transform to the ONTAP management endpoint (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/VMware-Migration-EC2-ONTAP/3421f0b8d162eff3f612f544acdf99f624411a0d/docs/_assets/images/png/atx-fsxn-control-path-en-dark%402x.png" rel="noopener noreferrer"&gt;dark theme&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The point is that the boot area and the data area go to different places. &lt;strong&gt;The boot column stays Amazon EBS in all three rows, and only the data column changes to FSx for ONTAP from the staging row on.&lt;/strong&gt; The target instance boots from EBS and receives the data over iSCSI. The source in this test was EC2, but the lower two rows of the configuration are the same whether the source is a VMware datastore or on-premises block storage.&lt;/p&gt;

&lt;p&gt;For the control path in Figure 2, the official blog says a PrivateLink connection is established automatically, but in practice a Network Load Balancer (NLB) and a VPC endpoint service are created inside your VPC. They incur hourly and LCU charges.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Normal operation (replicating)&lt;/th&gt;
&lt;th&gt;After cutover&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source EC2&lt;/td&gt;
&lt;td&gt;Running. The agent keeps sending blocks&lt;/td&gt;
&lt;td&gt;Can be stopped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replication server EC2&lt;/td&gt;
&lt;td&gt;MGN launches it automatically&lt;/td&gt;
&lt;td&gt;Terminated by Finalize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon EBS&lt;/td&gt;
&lt;td&gt;Boot staging&lt;/td&gt;
&lt;td&gt;Becomes the target's root volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;Staging FlexVol and LUN&lt;/td&gt;
&lt;td&gt;Target FlexVol (FlexClone)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network Load Balancer&lt;/td&gt;
&lt;td&gt;Running&lt;/td&gt;
&lt;td&gt;Remains after Finalize (manual deletion needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Table 1: each layer after cutover: the one that needs manual deletion is the Network Load Balancer&lt;/p&gt;


&lt;h2&gt;
  
  
  The Most Important Distinction: What Became GA Is the MGN Target
&lt;/h2&gt;

&lt;p&gt;Mix this up and you will evaluate the wrong product. It is easily confused with "FSx for ONTAP can be used as a VMware datastore".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Current support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can FSx for ONTAP be used as the MGN target storage type?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (GA with this release)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is FSx for ONTAP presented as a VMware datastore?&lt;/td&gt;
&lt;td&gt;No. The datastore is a separate feature on the Amazon EVS side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can the boot disk also go on FSx for ONTAP?&lt;/td&gt;
&lt;td&gt;No. Boot is always Amazon EBS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can EBS and FSx for ONTAP be mixed within one server?&lt;/td&gt;
&lt;td&gt;No. All data volumes use the same type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can you migrate agentlessly?&lt;/td&gt;
&lt;td&gt;No. Agent-based replication only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which protocol do clients connect with?&lt;/td&gt;
&lt;td&gt;iSCSI. The data is placed as LUNs inside a FlexVol&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Table 2: questions and current support: the agentless path is not supported&lt;/p&gt;

&lt;p&gt;The Known limitations in the MGN documentation state &lt;code&gt;Agent-based replication only&lt;/code&gt;. If you were planning around vCenter's agentless path, the plan has to change. The path depends on whether you leave VMs for EC2, or keep the VMs and move only the storage out.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifkdinxlwqr195t8zjlw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifkdinxlwqr195t8zjlw.png" alt="The replication template edit screen of AWS Transform MGN. The Amazon FSx for NetApp ONTAP configuration section has two inputs: Storage Virtual Machine (SVM) ID and FSx Storage Secret ARN. The note above them reads: Data disks will be migrated to FSx for ONTAP. Boot disk is always migrated to EBS as required by Amazon EC2." width="800" height="426"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3: the FSx for ONTAP settings in the replication template. Only the SVM ID and the Secret ARN are specified, and the note states that boot goes to EBS&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Environments This Configuration Suits
&lt;/h2&gt;

&lt;p&gt;Whether a migration fits depends on the shape of the workload, not on the product of the source storage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The boot / root area and the data area are separate, and the data area is on block storage&lt;/li&gt;
&lt;li&gt;You want capabilities such as Snapshot and thin provisioning on the data area after migration&lt;/li&gt;
&lt;li&gt;You want to turn "migrate, then move the data" into a single step&lt;/li&gt;
&lt;li&gt;You already run ONTAP and want the same mechanisms (Snapshot / FlexClone / SnapMirror) after migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of these apply whatever the source storage is. Even when the source is ONTAP, however, permissions, quotas and Snapshot policies are not migrated and need to be set up again.&lt;/p&gt;
&lt;h3&gt;
  
  
  Environments Where Another Option Fits Better for Now
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Boot and data sit on the same volume&lt;/li&gt;
&lt;li&gt;You cannot install an agent (agentless is not supported)&lt;/li&gt;
&lt;li&gt;The source is Windows and you want to move the data as SMB shares&lt;/li&gt;
&lt;li&gt;File-level migration is enough and block-level replication is not needed (a file transfer such as AWS DataSync is sufficient)&lt;/li&gt;
&lt;li&gt;The source is ONTAP and you want to take the volumes as they are (SnapMirror takes fewer steps)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  A Mini Glossary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Where it appears in this article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SVM&lt;/td&gt;
&lt;td&gt;A logical server inside the file system&lt;/td&gt;
&lt;td&gt;Where MGN creates the FlexVol and LUN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexVol&lt;/td&gt;
&lt;td&gt;A volume that holds data. LUNs live inside it too&lt;/td&gt;
&lt;td&gt;MGN creates one for staging and one for the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LUN&lt;/td&gt;
&lt;td&gt;A unit of block storage, exposed over iSCSI&lt;/td&gt;
&lt;td&gt;One source disk becomes one LUN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot&lt;/td&gt;
&lt;td&gt;A point-in-time image inside a volume&lt;/td&gt;
&lt;td&gt;MGN's SNAPSHOT phase creates it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexClone&lt;/td&gt;
&lt;td&gt;A writable clone made from a Snapshot&lt;/td&gt;
&lt;td&gt;The target FlexVol is one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split&lt;/td&gt;
&lt;td&gt;Detaching a FlexClone from its parent and materializing it&lt;/td&gt;
&lt;td&gt;Finalize runs it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  The Procedure and the Measured Time for Each Step
&lt;/h2&gt;

&lt;p&gt;The entry point is the AWS Transform workspace. MGN runs the migration itself, but it is AWS Transform that handles everything from planning to landing zone and network as a single job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Initialize MGN&lt;/td&gt;
&lt;td&gt;Done from the console. The CLI failed (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Install the agent&lt;/td&gt;
&lt;td&gt;Failed once (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Initial full sync (16 GiB / 3 disks)&lt;/td&gt;
&lt;td&gt;226 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Test launch&lt;/td&gt;
&lt;td&gt;SNAPSHOT 43 seconds, CONVERSION about 4 minutes 30 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Cutover (correct order)&lt;/td&gt;
&lt;td&gt;MGN job 649 seconds; 817 seconds from issuing it to confirming boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Finalize&lt;/td&gt;
&lt;td&gt;Split under 60 seconds; about 33 minutes until clean-up completed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(&lt;em&gt;n=1 measurements at 16 GiB / 3 disks. Fixed overhead dominates, so they cannot be used as a basis for RTO.&lt;/em&gt;)&lt;/p&gt;


&lt;h2&gt;
  
  
  Snapshot and FlexClone: What AWS Transform's Phase Names Refer To
&lt;/h2&gt;

&lt;p&gt;I lined up the ONTAP side by timestamp to see which ONTAP operation each AWS Transform phase name corresponds to.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AWS Transform phase&lt;/th&gt;
&lt;th&gt;Corresponding ONTAP operation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SNAPSHOT&lt;/td&gt;
&lt;td&gt;Creating a volume Snapshot of the staging FlexVol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LAUNCH&lt;/td&gt;
&lt;td&gt;Creating a FlexClone from that Snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The SNAPSHOT phase is not a plain data copy but a metadata operation (44 seconds for 8 GiB). The target volume is a clone with &lt;code&gt;is_flexclone: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What matters most is that &lt;strong&gt;a FlexClone uses almost no physical capacity when it is created&lt;/strong&gt;.&lt;br&gt;
Measured just before Finalize, the target volume's logical size was 7.91 GiB while its physical consumption was only 35.5 MiB. Capacity planning has to look at physical values, not logical ones.&lt;/p&gt;


&lt;h2&gt;
  
  
  Finalize Is Not Clean-up; It Is Where Capacity Peaks
&lt;/h2&gt;

&lt;p&gt;Running Finalize starts a split of the FlexClone (the operation that detaches it and makes it independent), which materializes the data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Elapsed from T0&lt;/th&gt;
&lt;th&gt;Observed change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Immediately&lt;/td&gt;
&lt;td&gt;Lifecycle &lt;code&gt;CUTOVER&lt;/code&gt;, replication &lt;code&gt;DISCONNECTED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;About 3 minutes&lt;/td&gt;
&lt;td&gt;FlexClone split starts. Physical consumption rising from 35.5 MiB → 3.94 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;About 4 minutes&lt;/td&gt;
&lt;td&gt;Split completes. Physical consumption 8.57 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;About 13 minutes&lt;/td&gt;
&lt;td&gt;Staging FlexVol deleted, replication server EC2 terminated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There was a gap of about 9 minutes between the split completing and the staging volume being deleted.&lt;br&gt;
&lt;strong&gt;During those 9 minutes, physical capacity holds twice the migrated data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you run Finalize while the aggregate's free space is below the migrated data size, it is likely to get stuck here. Keep in mind that Finalize is a capacity risk, not an availability risk.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Sizing Guideline Is the Official "3x", Not the Measured "2x"
&lt;/h3&gt;

&lt;p&gt;That peak in physical consumption (about 2x the migrated data) is a single value I observed in a 16 GiB environment. It shows how far physical capacity momentarily grew during Finalize, which is a different question from &lt;strong&gt;how much to provision the file system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The official documentation gives &lt;strong&gt;3x the migration data&lt;/strong&gt; as the SSD capacity to provision (&lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/fsx-ontap.html" rel="noopener noreferrer"&gt;MGN: FSx for ONTAP configuration&lt;/a&gt;, Step 2, "Storage capacity"). The 3x breaks down into three consumers that hold capacity at the same time during migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The replicated data (staging)&lt;/li&gt;
&lt;li&gt;The converted volumes used for launch (the target FlexClone)&lt;/li&gt;
&lt;li&gt;The original volumes pending deletion (the staging FlexVol)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The reason the official figure is 3x even though I measured about 2x is that volume deletion is a background operation, so the freed capacity is not available immediately after deletion.&lt;/strong&gt; Hence the guidance to provision the headroom up front. The documentation also recommends keeping SSD utilization at or below 80% throughout the migration.&lt;/p&gt;

&lt;p&gt;If you read this article and conclude "it measured 2x, so size at 2x", you fall below the official guideline (3x) and are more likely to hit a capacity shortfall during Finalize. &lt;strong&gt;Base the provisioned figure on the official 3x, not on the measured peak.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Four Places I Stumbled
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. A Failed Final Snapshot Behind a Successful Job
&lt;/h3&gt;

&lt;p&gt;To measure downtime, I shut down the source OS and then ran cutover. The job returned &lt;code&gt;COMPLETED&lt;/code&gt; and the target &lt;code&gt;LAUNCHED&lt;/code&gt;. It looked like a success, but the job log recorded this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;09:14:21  SNAPSHOT_START
09:19:22  SNAPSHOT_FAIL            timed out after 300 seconds
09:19:23  USING_PREVIOUS_SNAPSHOT

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final sync did not complete, and the job fell back to the previous snapshot. The cause was my procedure: shutting down the OS also stopped the replication agent, and taking the crash-consistent snapshot timed out.&lt;/p&gt;

&lt;p&gt;The correct procedure is to stop only the application's writes and run cutover with the OS and the agent still running. "Stop writes on the source" in the official documentation does not mean shutting down the OS.&lt;/p&gt;

&lt;p&gt;In this test there were no recent writes, so the data matched, but doing this in a real environment &lt;strong&gt;loses the latest data written after the snapshot it fell back to.&lt;/strong&gt; What makes this risky is that it does not show up in the job status (&lt;code&gt;status&lt;/code&gt; or &lt;code&gt;launchStatus&lt;/code&gt;) at all. By the API's design, the job returns &lt;code&gt;COMPLETED&lt;/code&gt; as long as processing finishes, even after a fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: After cutover, always check &lt;code&gt;event&lt;/code&gt; in &lt;code&gt;describe-job-log-items&lt;/code&gt;. A &lt;code&gt;COMPLETED&lt;/code&gt; job is not evidence that the final sync succeeded.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Two Unbootable Targets: NVMe Device Names Re-enumerated
&lt;/h3&gt;

&lt;p&gt;I ran cutover in the correct order twice, and both produced instances that would not boot (a UEFI shell reboot loop). The job log showed no abnormal events, and the status was &lt;code&gt;COMPLETED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Examining the boot EBS volume of an unbootable instance, &lt;strong&gt;the 8 GiB boot volume held the contents of a 4 GiB data disk.&lt;/strong&gt; The boot and data disk assignment had been swapped.&lt;/p&gt;

&lt;p&gt;The cause: when the source was stopped and restarted as part of the test, &lt;strong&gt;NVMe device names were re-enumerated and the boot disk moved from &lt;code&gt;/dev/nvme0n1&lt;/code&gt; to &lt;code&gt;/dev/nvme1n1&lt;/code&gt;&lt;/strong&gt;. MGN's staging-type assignment appears to be keyed on the device name, and it is not re-evaluated after the name moves.&lt;/p&gt;

&lt;p&gt;The official best practices say not to restart the source before cutover in the first place. If a restart does happen, check the disk assignment with a command like the one below and redo the test launch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check which device is treated as the boot disk, and its assignment&lt;/span&gt;
aws mgn get-replication-configuration &lt;span class="nt"&gt;--source-server-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'replicatedDisks[?isBootDisk==`true`].[deviceName,stagingDiskType]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. No Repair API, and Three Contradicting Errors
&lt;/h3&gt;

&lt;p&gt;I called &lt;code&gt;UpdateReplicationConfiguration&lt;/code&gt; to fix the disk assignment, but whatever parameters I passed, it returned contradicting errors, and the API could not fix it.&lt;/p&gt;

&lt;p&gt;In the end I deleted the source server and reinstalled the agent, which turned out to be unnecessary: rerunning the installer is the documented procedure.&lt;br&gt;
Rerunning the installer re-establishes the list of replicated disks and their mapping against the actual machine. To avoid losing launch template customizations, try rerunning the installer before deleting anything.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. The Installer's Hint Did Not Match the Real Cause
&lt;/h3&gt;

&lt;p&gt;When the agent installation failed, the installer displayed &lt;code&gt;Are kernel linux headers installed correctly?&lt;/code&gt;. Reading the log closely, the real cause was lack of space in &lt;code&gt;/tmp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Amazon Linux 2023, &lt;code&gt;/tmp&lt;/code&gt; is tmpfs; on a t3.small it has only 955 MiB, which does not meet the installer's requirement (1 GB or more free).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo env &lt;/span&gt;&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/tmp/mgn-build &lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/tmp/mgn-build &lt;span class="nv"&gt;TMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/tmp/mgn-build &lt;span class="se"&gt;\&lt;/span&gt;
  ./aws-replication-installer-init &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--no-prompt&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pointing it at &lt;code&gt;/var/tmp&lt;/code&gt; like this works around it. The installer's hint is not necessarily the real cause, so check the error code in the log.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shrink the Leftover Capacity After Finalize to Keep Costs Down
&lt;/h2&gt;

&lt;p&gt;The SSD capacity you provisioned at 3x for the migration is excessive once the migration is done. Leave it as is, and you keep paying for capacity you are not using. Here is what to keep in mind on cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Leftover Is Billed
&lt;/h3&gt;

&lt;p&gt;FSx for ONTAP storage has two tiers. The SSD tier (primary) is &lt;strong&gt;billed on the capacity you provision&lt;/strong&gt; — you pay for the GiB you reserve, not for the bytes you actually use (&lt;a href="https://aws.amazon.com/fsx/netapp-ontap/pricing/" rel="noopener noreferrer"&gt;Amazon FSx for NetApp ONTAP Pricing&lt;/a&gt;). The other tier, the capacity pool, is billed only for what you consume.&lt;/p&gt;

&lt;p&gt;Right after migration, of the SSD capacity reserved at 3x the migration data, the reserved amount stays even after the staging and pending-deletion volumes are gone. &lt;strong&gt;Even when the real data drops back to 1, if the SSD tier is still reserved at 3, you keep paying for the 2 of unused capacity.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What to Do: On Second-Generation, Decrease SSD Capacity After Migration
&lt;/h3&gt;

&lt;p&gt;Second-generation file systems (the &lt;code&gt;Single-AZ 2&lt;/code&gt; / &lt;code&gt;Multi-AZ 2&lt;/code&gt; deployment types) let you decrease SSD capacity after migration completes (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/storage-capacity-and-IOPS.html" rel="noopener noreferrer"&gt;File system storage capacity and IOPS&lt;/a&gt;). The documentation itself lists "after a large-scale data migration where temporary extra capacity was used" as a typical case for decreasing capacity. The steps and constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First-generation (&lt;code&gt;Single-AZ 1&lt;/code&gt; / &lt;code&gt;Multi-AZ 1&lt;/code&gt;) can increase but not decrease.&lt;/strong&gt; If your plan is to reserve a large amount temporarily for migration, choosing second-generation is the safer choice on cost.&lt;/li&gt;
&lt;li&gt;Each decrease is in steps of &lt;strong&gt;at least 9% of the current capacity&lt;/strong&gt;, down to a size where SSD utilization stays at or below 80% after the decrease.&lt;/li&gt;
&lt;li&gt;During the decrease operation you are &lt;strong&gt;billed for both the existing and the new size&lt;/strong&gt; (for example, decreasing 10 TiB → 5 TiB is billed at 15 TiB during the operation, 5 TiB after). The decrease can take from a few hours to a few weeks.&lt;/li&gt;
&lt;li&gt;After changing SSD capacity, IOPS, or throughput, you must wait a &lt;strong&gt;6-hour cooldown&lt;/strong&gt; before the next change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A decrease operation pauses while a FlexClone exists.&lt;/strong&gt; The MGN target volume is already independent after the Finalize split, so it qualifies; but with a FlexClone still present from before or during Finalize, the decrease will not progress. Run the decrease after Finalize completes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alternative: Stand Up a Migration-Only File System and Move to Production with SnapMirror
&lt;/h3&gt;

&lt;p&gt;The decrease has constraints. First-generation cannot shrink; even second-generation has the 9% step, the 80% ceiling and the 6-hour cooldown, is billed at both sizes during the operation, and can take weeks. So another design is to separate the migration landing spot and the production home from the start.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stand up a migration-only file system provisioned at 3x. Make it the ATX (MGN) migration target.&lt;/li&gt;
&lt;li&gt;Run through Finalize and let the target volume become independent.&lt;/li&gt;
&lt;li&gt;Stand up a separate production file system at the minimum size that fits the real data.&lt;/li&gt;
&lt;li&gt;Replicate the volume from the migration-only to the production file system with &lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/migrating-fsx-ontap-snapmirror.html" rel="noopener noreferrer"&gt;NetApp SnapMirror&lt;/a&gt;. SnapMirror is block-level replication that carries the deduplicated and compressed state and the snapshots across intact. It supports in-Region replication between two FSx for ONTAP file systems (&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/scheduled-replication.html" rel="noopener noreferrer"&gt;Replicating your data using NetApp SnapMirror&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Switch clients to the production file system and delete the migration-only file system in full.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This alternative helps &lt;strong&gt;when you use first-generation, or when you want to shrink substantially after migration&lt;/strong&gt;. The decrease is an operation that shrinks a file system and is unavailable on first-generation; SnapMirror moves the contents to a separate file system and so is not bound by the generation. Delete the migration-only side and billing stops along with the whole 3x reservation. AWS also presents SnapMirror as a way to migrate from first-generation to second-generation (&lt;a href="https://aws.amazon.com/blogs/modernizing-with-aws/migrating-sql-server-fci-storage-from-amazon-fsx-for-netapp-ontap-gen-1-to-gen-2/" rel="noopener noreferrer"&gt;Migrating SQL Server FCI Storage from FSx for ONTAP Gen 1 to Gen 2&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It does add steps, though. You go through cluster peering → SVM peering → creating the SnapMirror relationship → initialize → final update → break → mount, and the migration-only and production file systems run in parallel for a while, so both are billed during that time. &lt;strong&gt;In this article the alternative was only checked against the documentation and not run on real hardware (not verified).&lt;/strong&gt; Which of the decrease and SnapMirror is cheaper and faster depends on data size, generation and the shrink ratio, so estimate it for your own conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Measures Beyond the Decrease
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier infrequently accessed data to the capacity pool.&lt;/strong&gt; A volume's tiering policy moves unused data to the cheaper tier. The lower the SSD utilization, the smaller the size you can decrease to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce throughput capacity after migration too.&lt;/strong&gt; You provision it high for speed during migration, and throughput is also billed. The documentation states you can reduce throughput after migration completes (&lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/fsx-ontap.html" rel="noopener noreferrer"&gt;MGN: FSx for ONTAP configuration&lt;/a&gt;, "Throughput capacity"). During an SSD decrease operation, however, changing SSD, IOPS, and throughput at once is constrained, so do them one at a time with the cooldown in between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the NLB and the VPC endpoint service.&lt;/strong&gt; This is separate from capacity, but left alone they keep incurring hourly and LCU charges (see the teardown checklist below).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Teardown Checklist
&lt;/h2&gt;

&lt;p&gt;Finalize does not finish the clean-up. The correct teardown order for leaving nothing behind is as follows (steps 1–7 depend on each other).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Terminate the target and source EC2 instances&lt;/li&gt;
&lt;li&gt;Delete the MGN source server&lt;/li&gt;
&lt;li&gt;Reject the VPC endpoint connection, then delete the endpoint service&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete the NLB and its target group (important: charges continue until you delete them by hand)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Delete the target FlexVol with &lt;code&gt;fsx delete-volume&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Delete the SVM with &lt;code&gt;fsx delete-storage-virtual-machine&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Delete the ONTAP &lt;code&gt;security login&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The staging EBS volumes are deleted automatically with a delay, but the target's root EBS volume has no &lt;code&gt;DeleteOnTermination&lt;/code&gt; and must be deleted manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Initializing AWS Transform from the CLI: Create the IAM Roles First
&lt;/h2&gt;

&lt;p&gt;If running &lt;code&gt;aws mgn initialize-service&lt;/code&gt; from the API or CLI fails, the cause is that the IAM roles have not been created.&lt;br&gt;
Initializing from the console creates the IAM roles for you, but on the CLI path you need to create eight roles (including &lt;code&gt;AWSApplicationMigrationFsxProxyRole&lt;/code&gt; for FSx for ONTAP) and attach their policies first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;With this GA, a migration now finishes with the data area already on FSx for ONTAP as iSCSI LUNs, and the step of moving the data afterwards is gone.&lt;/p&gt;

&lt;p&gt;Any server whose boot and data are separate fits the same shape, whatever the original storage.&lt;br&gt;
On the other hand, watch for the configuration spanning two kinds of storage, the physical capacity spike during Finalize, and MGN-specific traps such as "a job status of COMPLETED does not mean the final sync succeeded". Plan capacity as a round trip: provision the 3x SSD capacity the documentation calls for, then, once migration is done, hand back the surplus with a second-generation capacity decrease to bring the cost down.&lt;/p&gt;

&lt;p&gt;I hope this test record helps anyone migrating servers with separate boot and data to EC2 while considering FSx for ONTAP as the home for the data area.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-transform-fsx-netapp-ontap-support/" rel="noopener noreferrer"&gt;AWS What's New: AWS Transform announces general availability of Amazon FSx for NetApp ONTAP support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/fsx-ontap.html" rel="noopener noreferrer"&gt;MGN: FSx for ONTAP configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/best_practices_mgn.html" rel="noopener noreferrer"&gt;MGN: Best practices for AWS Transform MGN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/evs-ontap.html" rel="noopener noreferrer"&gt;FSx for ONTAP: Using Amazon Elastic VMware Service with FSx for ONTAP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yoshiki0705/VMware-Migration-EC2-ONTAP/blob/main/docs/en/atx-fsxn-ga-verification.md" rel="noopener noreferrer"&gt;Verification report (GitHub)&lt;/a&gt;: the test environment configuration, the measured teardown procedure, and the list of feedback sent to AWS, all omitted from this article&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;All test environments have been deleted. The figures are single measurements under a specific environment and conditions, and vary with data size and configuration. The configuration details are in the verification report above.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is Part 2 of the series "The Data Foundation for AWS Modernization." The Japanese original is on &lt;a href="https://hakobiya.hatenablog.com/entry/fsxn-aws-transform-mgn-migration-target" rel="noopener noreferrer"&gt;hatenablog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>netapp</category>
      <category>migration</category>
      <category>storage</category>
    </item>
    <item>
      <title>Designing AWS Modernization with VMware Migration as the Entry Point — Why FSx for ONTAP as the Data Foundation</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:42:08 +0000</pubDate>
      <link>https://dev.to/aws-builders/designing-aws-modernization-with-vmware-migration-as-the-entry-point-why-fsx-for-ontap-as-the-3k24</link>
      <guid>https://dev.to/aws-builders/designing-aws-modernization-with-vmware-migration-as-the-entry-point-why-fsx-for-ontap-as-the-3k24</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;What comes "next" for VMware workloads? Hardly a day goes by without the question coming up.&lt;/p&gt;

&lt;p&gt;The licensing changes that followed Broadcom's acquisition of VMware have pushed many organizations into a fundamental review of their virtualization strategy. But it is worth pausing here: &lt;strong&gt;"getting off VMware" is not the goal.&lt;/strong&gt; The real destination is the modernization beyond the migration — reworking applications and data, up to and including serverless and cloud-native architectures. The AWS Storage Blog, too, frames the migration not as mere license avoidance but as an opportunity to modernize infrastructure. &lt;a href="https://aws.amazon.com/blogs/storage/expedite-vmware-migration-to-amazon-ec2-and-amazon-fsx-for-netapp-ontap-using-bluexp-workload-factory-for-aws-migration-advisor/" rel="noopener noreferrer"&gt;(Reference: AWS Storage Blog — Expedite VMware migration to Amazon EC2 and Amazon FSx for NetApp ONTAP using the BlueXP workload factory migration advisor)&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core of Modernization on AWS — AWS Transform
&lt;/h3&gt;

&lt;p&gt;What drives modernization on AWS today is &lt;strong&gt;AWS Transform&lt;/strong&gt;, an agentic-AI service. Beyond rehosting VMware workloads, it can containerize source code (replatforming to Amazon ECS / Amazon EKS) and take .NET Framework applications to cross-platform .NET on Linux — capabilities that &lt;strong&gt;run migration and modernization in parallel&lt;/strong&gt;. It lifts migration from "moving servers" to "reworking applications," and that is what modernization on AWS looks like now.&lt;/p&gt;

&lt;p&gt;However you rework the compute — EC2, containers, or serverless — one thing is always needed: &lt;strong&gt;a place for the data&lt;/strong&gt;. This is where &lt;strong&gt;Amazon FSx for NetApp ONTAP&lt;/strong&gt;, which I work with, comes alongside.&lt;/p&gt;

&lt;p&gt;I field questions about FSx for ONTAP every day, and one thing these migrations tend to overlook is &lt;strong&gt;the continuity and extensibility of the storage operations model&lt;/strong&gt;. Keep the storage operations built up over years in VMware and ONTAP environments — Snapshot, Clone, Replication, Storage Efficiency — on AWS too, and let a multi-protocol data hub support everything from migration through to cloud-native. Keep the data layer consistent and you can re-choose the compute as many times as you like. &lt;strong&gt;Designing modernization with AWS migration as the entry point and the data foundation at the core&lt;/strong&gt; — that is the lens of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Destinations for VMware Workloads
&lt;/h2&gt;

&lt;p&gt;AWS officially presents five pathways for VMware workloads. &lt;a href="https://aws.amazon.com/vmware/explore/" rel="noopener noreferrer"&gt;(Reference: AWS for VMware — Comprehensive Pathways)&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS for VMware: The Pathways to Migrate and Modernize on AWS
&lt;/h3&gt;

&lt;p&gt;I drew the five official pathways as five rows, combining AWS services with ONTAP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Row 1 is Migrate to Amazon EC2: Amazon EC2 reaches Amazon FSx for NetApp ONTAP over iSCSI.&lt;/li&gt;
&lt;li&gt;Row 2 is Modernize on AWS: Amazon Elastic Container Service, Amazon Elastic Kubernetes Service, AWS Fargate, Amazon WorkSpaces, AWS Lambda, AWS Batch and Amazon FSx for NetApp ONTAP.&lt;/li&gt;
&lt;li&gt;Row 3 is Run VMware on AWS: Amazon Elastic VMware Service reaches Amazon FSx for NetApp ONTAP over NFS / SMB.&lt;/li&gt;
&lt;li&gt;Row 4 is Run AWS on-premises: AWS Outposts and NetApp ONTAP.&lt;/li&gt;
&lt;li&gt;Row 5 is Run third-party hypervisors on AWS: Red Hat OpenShift Service on AWS, Amazon FSx for NetApp ONTAP, and Nutanix Cloud Clusters on AWS.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3tsogarfy4g89hihu60.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3tsogarfy4g89hihu60.png" alt="Figure 1: the five official AWS pathways to migrate and modernize on AWS. Amazon FSx for NetApp ONTAP can sit in the data layer of any of them" width="800" height="1204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/VMware-Migration-EC2-ONTAP/471435e0e879bc08e3c844cc5b1ada122c7d2396/docs/_assets/images/png/atx-fsxn-vmware-pathways-en-dark%402x.png" rel="noopener noreferrer"&gt;dark theme&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: &lt;a href="https://aws.amazon.com/vmware/partner-offerings/" rel="noopener noreferrer"&gt;AWS for VMware Partner Offerings&lt;/a&gt;&lt;br&gt;
— "AWS offers the most comprehensive set of migration and modernization options for&lt;br&gt;
VMware-based workloads - from relocating to Amazon EVS, to rehosting on Amazon EC2,&lt;br&gt;
containerizing with Amazon EKS, or transitioning to running third-party hypervisors&lt;br&gt;
in the cloud like ROSA and NC2 on AWS." (Summarized; rephrased for licensing constraints.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Each Pathway in Detail
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Rehost to Amazon EC2
&lt;/h4&gt;

&lt;p&gt;The most straightforward path: migrating VMware VMs as EC2 instances. AWS Transform for VMware (an agentic-AI-based migration service) became generally available in May 2025. &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/05/aws-transform-vmware-generally-available/" rel="noopener noreferrer"&gt;(Reference: AWS What's New)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Separately, on 30 August 2026, &lt;strong&gt;FSx for ONTAP became generally available as a target storage type for AWS Transform for migrations (MGN)&lt;/strong&gt;. The former is a service for migrating from VMware; the latter is a target-storage choice for server migration. They are different features, and Part 2 of this series covers the latter. &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-transform-fsx-netapp-ontap-support/" rel="noopener noreferrer"&gt;(Reference: AWS What's New)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage configuration options:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EBS only&lt;/strong&gt;: Simple. Automated with MGN / AWS Transform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EBS (OS) + FSx for ONTAP (Data)&lt;/strong&gt;: Keeps ONTAP capabilities. Supported by AWS Transform (MGN) / Shift Toolkit / Cirrus Migrate Cloud (&lt;strong&gt;the scope of this series&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Modernization (Containers / Serverless)
&lt;/h4&gt;

&lt;p&gt;As the next step after rehosting to EC2, you can modernize according to the characteristics of each workload.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Suitable workloads&lt;/th&gt;
&lt;th&gt;FSx for ONTAP integration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECS / EKS (EC2 mode)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stateful containers (DB, middleware)&lt;/td&gt;
&lt;td&gt;✅ iSCSI / NFS mount available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECS / EKS (Fargate)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stateless microservices&lt;/td&gt;
&lt;td&gt;△ Through EFS only (no iSCSI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Lambda&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Event-driven, short-running processing&lt;/td&gt;
&lt;td&gt;△ EFS mount available, no iSCSI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Batch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Batch processing / HPC&lt;/td&gt;
&lt;td&gt;✅ iSCSI available in EC2 mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon WorkSpaces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;VDI (virtual desktops)&lt;/td&gt;
&lt;td&gt;✅ FSx for ONTAP file shares&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not about containerizing VMs as they are. The journey is "rehost to EC2 → containerize the application → move step by step to Fargate/Lambda".&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Amazon EVS (Keep VMware on AWS)
&lt;/h4&gt;

&lt;p&gt;With Amazon Elastic VMware Service, you can deploy VMware Cloud Foundation (VCF) directly on EC2 bare metal inside your VPC. You keep your existing vSphere skill set and can connect FSx for ONTAP as an external datastore.&lt;br&gt;
It suits cases with a large VMware-dependent application estate, where leaving VMware in a short time is impractical.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. AWS Outposts (On-premises AWS + NetApp External Storage)
&lt;/h4&gt;

&lt;p&gt;AWS Outposts is a fully managed service that places AWS infrastructure on premises.&lt;br&gt;
Third-party block storage integration was announced in December 2024, and NetApp ONTAP and StorageGRID are available as partners validated through the AWS Service Ready Program. &lt;a href="https://aws.amazon.com/blogs/compute/new-simplifying-the-use-of-third-party-block-storage-with-aws-outposts/" rel="noopener noreferrer"&gt;(Reference: AWS Blog)&lt;/a&gt; &lt;a href="https://netapp.com/aws/outposts/" rel="noopener noreferrer"&gt;(Reference: NetApp)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NetApp ONTAP iSCSI LUNs can be attached directly from the AWS console as data volumes for EC2 instances, and boot volume support was added in July 2025.&lt;br&gt;
This gives you &lt;strong&gt;separation of compute and storage&lt;/strong&gt;: "compute is AWS-managed, storage is your existing ONTAP."&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Partner Solutions (ROSA / Expanding Nutanix + NetApp Integration)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Red Hat OpenShift Service on AWS (ROSA):&lt;/strong&gt; A fully managed environment based on OpenShift. With the NetApp Trident CSI driver, ROSA Pods can access FSx for ONTAP over NFS/iSCSI. &lt;a href="https://aws.amazon.com/rosa/" rel="noopener noreferrer"&gt;(Reference)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nutanix + NetApp ONTAP (on-premises Nutanix Cloud Platform):&lt;/strong&gt; At .NEXT Chicago in April 2026, a partnership was announced that lets the Nutanix Cloud Platform (NCP / AHV) use NetApp ONTAP (AFF / FAS) as &lt;strong&gt;external storage&lt;/strong&gt;. Over an NFS-based connection, compute (Nutanix AHV) and storage (ONTAP) scale independently. GA is planned for &lt;strong&gt;the second half of 2026 (NetApp targets Q3 2026, with Early Access underway)&lt;/strong&gt;. &lt;strong&gt;The partnership covers on-premises NetApp enterprise storage; integration with Nutanix Cloud Clusters (NC2) on AWS or with FSx for ONTAP is not announced or committed in public sources&lt;/strong&gt; (NetApp mentions "NetApp cloud storage services for Nutanix Cloud Platform" as a future direction, with the timing and the services undetermined). &lt;a href="https://www.netapp.com/blog/modernize-virtualization-nutanix-partnership/" rel="noopener noreferrer"&gt;(Reference: NetApp Blog, GA Q3 2026)&lt;/a&gt; &lt;a href="https://www.nutanix.com/press-releases/2026/nutanix-and-netapp-form-strategic-alliance" rel="noopener noreferrer"&gt;(Reference: Nutanix press release)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The NetApp ecosystem as a whole:&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxbr1uqg63kihg2d9bpi.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxbr1uqg63kihg2d9bpi.png" alt="On-premises on the left, AWS on the right. On the left, a hypervisor (VMware ESXi, Nutanix AHV or Hyper-V) uses NetApp ONTAP (FAS / AFF) as its datastore, holding the VMDK, VHDX or LUN. On the right, Amazon EC2 uses Amazon FSx for NetApp ONTAP. The on-premises ONTAP and Amazon FSx for NetApp ONTAP are connected bidirectionally by SnapMirror. Snapshot, FlexClone, SnapMirror and Storage Efficiency are the same on both sides" width="800" height="426"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: the data layer stays the same when the compute changes. SnapMirror is what crosses the boundary (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/VMware-Migration-EC2-ONTAP/471435e0e879bc08e3c844cc5b1ada122c7d2396/docs/_assets/images/png/atx-fsxn-ontap-portability-en-dark%402x.png" rel="noopener noreferrer"&gt;dark theme&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What this figure shows is that &lt;strong&gt;ONTAP is a data platform independent of the choice of hypervisor or cloud&lt;/strong&gt;. Whether you move the compute layer from VMware to EC2 or to Nutanix, the data layer stays consistently available, and SnapMirror moves and protects the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where This Series Sits
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzntir72a71z6wvw6adx9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzntir72a71z6wvw6adx9.png" alt="From VMware ESXi as the current state, the diagram runs downwards through Phase 1 rehost, Phase 2 replatform and Phase 3 refactor. Phase 1 is Amazon EC2 reaching Amazon FSx for NetApp ONTAP over iSCSI multipath. Phase 2 holds Amazon Elastic Container Service, Amazon Elastic Kubernetes Service, AWS Batch, AWS Parallel Computing Service, AWS Deadline Cloud, Amazon CloudFront, AWS Transfer Family, S3 Access Points and Amazon FSx for NetApp ONTAP. Phase 3 holds AWS Fargate, AWS Lambda, Amazon Simple Storage Service, Amazon DynamoDB, S3 Access Points and Amazon FSx for NetApp ONTAP. A separate box above lists the alternatives to rehosting: Amazon Elastic VMware Service, Nutanix Cloud Clusters on AWS and Red Hat OpenShift Service on AWS" width="800" height="1149"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3: the phases beyond rehosting, and the alternatives to it. This series covers Phase 1 (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/VMware-Migration-EC2-ONTAP/471435e0e879bc08e3c844cc5b1ada122c7d2396/docs/_assets/images/png/atx-fsxn-migration-journey-en-dark%402x.png" rel="noopener noreferrer"&gt;dark theme&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This series concentrates on &lt;strong&gt;Phase 1 (rehost)&lt;/strong&gt;, but the EC2 + FSx for ONTAP configuration is designed not to close off the path to Phase 2 and beyond. FSx for ONTAP is reachable over NFS/iSCSI from ECS/EKS as well as from EC2, so the data layer can stay as it is after containerization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "EC2 + FSx for ONTAP"
&lt;/h2&gt;

&lt;p&gt;Among the five pathways, here is why this series looks at "EC2 + FSx for ONTAP" in particular.&lt;/p&gt;

&lt;h3&gt;
  
  
  An Entry Point for Rehosting and a Foundation for Modernization
&lt;/h3&gt;

&lt;p&gt;This configuration is not just a rehost destination; it is designed not to close off future modernization.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Now (Phase 1)&lt;/strong&gt;: Rehost VMs to EC2. Data disks on FSx for ONTAP iSCSI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next (Phase 2)&lt;/strong&gt;: Containerize the application. Keep accessing FSx for ONTAP over NFS/iSCSI from ECS/EKS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Later (Phase 3)&lt;/strong&gt;: Parts that became stateless move to Fargate/Lambda. The FSx for ONTAP data layer stays as it is&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The multi-protocol access of FSx for ONTAP (NFS/SMB/iSCSI) is what supports this step-by-step migration. A volume used as an iSCSI LUN for EC2 can later be NFS-mounted by an EKS Pod.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Value of ONTAP Is Capability, Not Capacity
&lt;/h3&gt;

&lt;p&gt;Seen as just "large-capacity storage," FSx for ONTAP can look expensive next to EBS. But capacity is not where the value of ONTAP lies.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ONTAP feature&lt;/th&gt;
&lt;th&gt;Use on AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Point-in-time copies in seconds. Instant test environments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FlexClone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clones without copying data. Lower cost for dev/test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SnapMirror&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Block-level replication. Cross-Region DR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compression / Dedup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower effective capacity. Most effective for databases and logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Thin Provisioning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pay for what is used. Avoid over-provisioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Access the same volume over NFS/SMB/iSCSI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  FSx for ONTAP EC2 Integration Pattern
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnul5t8wk9s0zcdscj42b.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnul5t8wk9s0zcdscj42b.png" alt="A table-shaped diagram pairing three Amazon EC2 instances with their boot / root volume and their data volume. The rows are, from the top, source, staging and after cutover. The columns are the boot / root volume and the data volume. The boot column is Amazon Elastic Block Store in all three rows. In the data column, only the source row is Amazon Elastic Block Store; staging and after cutover are Amazon FSx for NetApp ONTAP" width="800" height="793"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4: the boot column is Amazon EBS in all three rows; only the data column changes to FSx for ONTAP from staging on (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/VMware-Migration-EC2-ONTAP/471435e0e879bc08e3c844cc5b1ada122c7d2396/docs/_assets/images/png/atx-fsxn-data-path-en-dark%402x.png" rel="noopener noreferrer"&gt;dark theme&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This configuration has the following advantages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage and compute scale independently&lt;/strong&gt;: Change FSx for ONTAP capacity/throughput without stopping EC2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ONTAP operations model carries over&lt;/strong&gt;: Operate snapshots, clones and replication with the same CLI/API as on premises&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The limit moves&lt;/strong&gt;: The ceiling is no longer the per-instance Amazon EBS limit but the EC2 network bandwidth and the FSx for ONTAP throughput capacity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Note: Performance Ceilings and Protocols
&lt;/h4&gt;

&lt;p&gt;"The limit moves" above does not mean the limit goes away. This series has not measured it yet, but verification in a sibling project (&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/a97a4ec/docs/ja/verification/perf-matrix-results.md#f-1-iscsi-%E3%81%AE%E5%AE%9F%E6%B8%AC" rel="noopener noreferrer"&gt;F-1 iSCSI measurement&lt;/a&gt; and others) has shown the following.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simple arithmetic between session count and bandwidth misses&lt;/strong&gt;
Estimating "required bandwidth ÷ 625 MBps = number of sessions" misses in both directions. One TCP connection came out at 1.82x the prediction and sixteen at 0.20x, so the ceiling is not set by the capacity of a single flow alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The performance gap between protocols (iSCSI vs NVMe/TCP) depends on the deployment&lt;/strong&gt;
Measured on the same file system, iSCSI and NVMe/TCP were 0.06% apart — practically the same — while on other configurations their order swapped. Quoting a past ratio as the basis for a protocol choice is unsafe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANA multipath is not available on Amazon Linux 2023&lt;/strong&gt;
&lt;code&gt;CONFIG_NVME_MULTIPATH&lt;/code&gt; is unset in all three AL2023 kernel series (&lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/a248d47/docs/ja/verification/perf-matrix-results.md#ana-%E3%83%9E%E3%83%AB%E3%83%81%E3%83%91%E3%82%B9%E3%81%8C-amazon-linux-2023-%E3%81%A7%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8" rel="noopener noreferrer"&gt;details&lt;/a&gt;). The ANA path makes a large difference to sequential reads, so verifying it enabled needs a different distribution (RHEL 9.3 or similar).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;These values come from tests against a raw device and include burst; they do not apply directly to this configuration, which puts a file system on the LUN.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a Migration Tool
&lt;/h2&gt;

&lt;p&gt;Even when the destination is "EC2 + FSx for ONTAP," the tool depends on the environment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Candidate tool&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You want the data volumes on FSx for ONTAP (whatever the source storage is)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AWS Transform (MGN)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Set FSx for ONTAP as the target storage type. Generally available since 30 August 2026. Boot is always Amazon EBS. Verified hands-on in Part 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already on an ONTAP NFS datastore + small to medium scale&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NetApp Shift Toolkit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Converts disks without copying them, using FlexClone. EC2 / FSx for ONTAP support is Early Preview. Verified hands-on in Part 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already on ONTAP + large scale (100+ VMs)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cirrus Migrate Cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YAML-driven automation. Paid, through AWS Marketplace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration planning and sizing only&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;AWS Application Discovery Service&lt;/strong&gt; / &lt;strong&gt;AWS Migration Evaluator&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Inventory collection and cost estimation. AWS native&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Where NetApp Shift Toolkit Fits
&lt;/h3&gt;

&lt;p&gt;Shift Toolkit converts VMs on an ONTAP NFS datastore without copying their disks. Conversion is done with FlexClone and metadata operations, so the conversion itself is quick.&lt;br&gt;
Migrations from VMware ESXi to Hyper-V, OpenShift Virtualization, Proxmox VE and OLVM are GA today, and &lt;strong&gt;support for VMware ESXi → Amazon EC2 / FSx for ONTAP is at the Early Preview stage&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Early Preview note: The current scope is configurations that place data disks on FSx for ONTAP. Specifications and constraints may change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What This Series Covers
&lt;/h2&gt;

&lt;p&gt;Starting from migration as the entry point, the series runs four parts through to the modernization beyond it. Parts 2 and 4 are the two migration paths; Part 3 is the modernization that follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Source storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Part 2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/aws-builders/aws-transform-now-supports-block-storage-migration-to-fsx-for-ontap-benefits-and-pitfalls-from-a-1hhe"&gt;AWS Transform Now Supports Block Storage Migration to FSx for ONTAP — Benefits and Pitfalls from a Hands-On EC2 Test&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GA (30 August 2026) / verified hands-on&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 3&lt;/td&gt;
&lt;td&gt;Beyond the AWS migration of a VMware environment: serverless, cloud-native, and the data foundation (ECS / EKS, S3 Access Points, DR)&lt;/td&gt;
&lt;td&gt;Upcoming&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 4&lt;/td&gt;
&lt;td&gt;NetApp Shift Toolkit&lt;/td&gt;
&lt;td&gt;EC2 / FSx for ONTAP support is Early Preview / verified hands-on&lt;/td&gt;
&lt;td&gt;An ONTAP NFS datastore&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of the two migration paths, AWS Transform is already GA and applies more widely, so the one a reader can try today (Part 2) comes first. Shift Toolkit (Part 4), which builds on existing ONTAP assets, comes later because its EC2 support is Early Preview. Part 3 sits between them and covers how to provide the data foundation when migrated workloads are reshaped into containers and serverless.&lt;/p&gt;

&lt;p&gt;Across the parts, we plan to cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Environment setup&lt;/strong&gt;: Design and build of VPC + FSx for ONTAP + VPN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration&lt;/strong&gt;: Migrating Linux / Windows VMs to EC2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modernization&lt;/strong&gt;: Containerization on ECS / EKS, Fargate with S3 Access Points, DR and cyber resiliency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational continuity&lt;/strong&gt;: Snapshot / Clone / SnapMirror behavior after migration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost comparison&lt;/strong&gt;: TCO against an EBS-only configuration&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migrating from VMware is not only about "where to go" but about "how to carry the storage operations model forward".&lt;/li&gt;
&lt;li&gt;For ONTAP users, FSx for ONTAP is a strong option for keeping, and making full use of, the value of ONTAP on AWS.&lt;/li&gt;
&lt;li&gt;Choose the tool by source storage and scale. If the source storage does not matter, AWS Transform (MGN); for ONTAP NFS datastores at small to medium scale, Shift Toolkit.&lt;/li&gt;
&lt;li&gt;Shift Toolkit's EC2 support is at the Early Preview stage, so those verification results may change at GA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/aws-transform-now-supports-block-storage-migration-to-fsx-for-ontap-benefits-and-pitfalls-from-a-1hhe"&gt;The next article (Part 2)&lt;/a&gt; covers the AWS Transform (MGN) path that sets FSx for ONTAP as the target storage, verified hands-on after GA. Part 3 covers the modernization beyond migration (serverless and cloud-native), and Part 4 explains how Shift Toolkit works — how FlexClone changes VM migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yoshiki0705/VMware-Migration-EC2-ONTAP" rel="noopener noreferrer"&gt;Verification repository (GitHub): VMware-Migration-EC2-ONTAP&lt;/a&gt; — procedures, configuration and figures for this series&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Adoption-Playbook/blob/main/docs/en/reference/modernization-journey-map.md" rel="noopener noreferrer"&gt;Modernization journey map (FSx for ONTAP Adoption Playbook)&lt;/a&gt; — which material and implementation pattern to read at each stage beyond migration&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/vmware/explore/" rel="noopener noreferrer"&gt;AWS for VMware — Comprehensive Pathways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/transform/vmware/" rel="noopener noreferrer"&gt;AWS Transform for VMware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/09/aws-transform-fsx-netapp-ontap-support/" rel="noopener noreferrer"&gt;AWS What's New: AWS Transform announces general availability of Amazon FSx for NetApp ONTAP support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/fsx-ontap.html" rel="noopener noreferrer"&gt;MGN: FSx for ONTAP configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nutanix.com/blog/netapp-and-nutanix-announce-technical-partnership" rel="noopener noreferrer"&gt;Nutanix Blog: NetApp and Nutanix Announce Technical Partnership&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/vmware/partner-offerings/" rel="noopener noreferrer"&gt;AWS for VMware Partner Offerings (ROSA, NC2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.netapp.com/us-en/netapp-solutions-virtualization/migration/shift-toolkit-overview.html" rel="noopener noreferrer"&gt;NetApp Shift Toolkit Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/storage/seamless-migration-from-any-vmware-environment-to-amazon-fsx-for-netapp-ontap-and-amazon-ec2/" rel="noopener noreferrer"&gt;AWS Storage Blog: Seamless migration from VMware to FSx for ONTAP and EC2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/storage/expedite-vmware-migration-to-amazon-ec2-and-amazon-fsx-for-netapp-ontap-using-bluexp-workload-factory-for-aws-migration-advisor/" rel="noopener noreferrer"&gt;AWS Storage Blog: Expedite VMware migration to Amazon EC2 and Amazon FSx for NetApp ONTAP using the BlueXP workload factory migration advisor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/fsx/netapp-ontap/" rel="noopener noreferrer"&gt;Amazon FSx for NetApp ONTAP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/evs/" rel="noopener noreferrer"&gt;Amazon Elastic VMware Service (EVS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/rosa/" rel="noopener noreferrer"&gt;Red Hat OpenShift Service on AWS (ROSA)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/apn/accelerate-vmware-migrations-to-aws-with-nutanix-nc2/" rel="noopener noreferrer"&gt;Nutanix Cloud Clusters on AWS (NC2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/vmware/migrationaccelerator/" rel="noopener noreferrer"&gt;AWS VMware Migration Accelerator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netapp.com/blog/simplify-vm-migration-shift-toolkit/" rel="noopener noreferrer"&gt;NetApp Blog: Simplify VM migration with Shift Toolkit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article is Part 1 of the series "The Data Foundation for AWS Modernization." The Japanese original is on &lt;a href="https://hakobiya.hatenablog.com/entry/fsxn-vmware-migration-options-ec2" rel="noopener noreferrer"&gt;hatenablog&lt;/a&gt;. Shift Toolkit's EC2 support is Early Preview, and its specifications are subject to change.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vmware</category>
      <category>netapp</category>
      <category>modernization</category>
    </item>
  </channel>
</rss>
