S3 Files: AWS Finally Solves the Object Storage to File System Problem

S3 Files: AWS Finally Solves the Object Storage to File System Problem

👁61views

S3 Files is AWS's new managed layer that presents S3 object storage as a mountable POSIX-like file system, removing the need to run and patch your own NFS or FSx gateway. It handles file semantics, caching, and consistency on top of S3 buckets, but it still carries object storage limits, performance tradeoffs, and setup gotchas that matter before production use.

CloudScale AI SEO - Article Summary
  • 1.
    What it is
    S3 Files is Amazon's new capability that exposes an S3 bucket or prefix as a shared NFS file system, and this covers what it actually does, how the two tier storage model works, plus setup, monitoring, costs and production gotchas.
  • 2.
    Why it matters
    Understanding S3 Files helps architects avoid unnecessary glue architecture, since it replaces workarounds like FUSE mount clients or manual EFS/FSx synchronisation for applications needing POSIX file semantics over S3 data.
  • 3.
    Key takeaway
    S3 Files does not eagerly copy your whole bucket into the fast storage layer; only selected metadata and active file data are synchronised, meaning object model quirks like rename and archive class handling still leak through.
~47 min read

A deep technical walkthrough of Amazon S3 Files: what it actually is, where it earns its complexity, setup scripts, monitoring, costs and the production gotchas that will bite you if you skip straight to the mount command.

Jump to: What it is · What it is not · Setup script · Performance tests · Monitoring · Gotchas · Production checklist · Outside AWS

This is a long piece, closer to a field guide than a quick read, because the argument and the runbook belong together. If you want to be selective rather than read it end to end, architects will get most of the value from sections 1 to 7 and 21 to 24, platform engineers from sections 8 to 18, operators from sections 14 to 22, and anyone running their own hardware at home should go straight to section 25.

1. The release that matters

Amazon S3 Files is one of those AWS releases that looks simple until you think about the amount of architecture it quietly collapses. On 7 April 2026, AWS announced S3 Files, a new capability that lets you expose an S3 general purpose bucket, or a prefix inside that bucket, as a shared file system to AWS compute. The important phrase here is not “mount S3”, because we have had ways to mount S3 like storage for years. The important phrase is “shared file system”, because S3 Files gives applications a file interface over S3 data while AWS manages the translation, synchronisation, metadata, permissions and active data performance layer underneath.

This changes one of the oldest architectural trade offs in AWS. S3 has always been the natural place to keep durable, cheap and massively scalable data, but many applications still expect files, directories, POSIX permissions (the standard Linux model of owner, group and other access rules), locks, renames and ordinary filesystem calls. Before S3 Files, you usually had to choose between writing directly to S3 using object APIs, copying S3 data into EFS (AWS’s managed NFS file service) or FSx (AWS’s family of managed file systems for Windows, NetApp, OpenZFS and Lustre workloads), mounting S3 through a client with limited semantics, or building your own synchronisation layer between object storage and a file system. S3 Files does not make all of those options obsolete, but it does make a large class of messy glue architecture look unnecessary. AWS describes the release as making general purpose S3 buckets accessible as native file systems from EC2, ECS, EKS, Fargate, Batch and Lambda, with S3 objects presented as files and directories through NFS (Network File System, the long established protocol Linux and Unix machines use to mount a remote file system as though it were local) v4.1 and later operations.

The trade off is that the object model still leaks at the edges, particularly around rename, conflict handling, archive storage classes and high churn metadata workloads. None of that is a flaw so much as a consequence of what S3 fundamentally is, but it is worth saying plainly before the rest of this piece gets enthusiastic about what the service unlocks.

The mental model is straightforward once you sit with it. S3 remains the durable system of record, while the file system becomes the active working interface on top of it, with reads and writes going through ordinary file operations rather than object calls. Underneath that interface, active data and metadata sit on a high performance storage layer, changes made through the mount are synchronised back to S3, and changes made directly in S3 are reflected back into the file system view. The result is not S3 pretending to be POSIX in the old, brittle, FUSE (a Linux mechanism that lets an ordinary program implement a file system in user space, the basis for most of the janky S3 mount clients that came before this) client sense, but a managed AWS service that deliberately bridges object and file semantics.

If you only take one thing from this article before deciding whether to bother, it is probably this decision table. A quick note on a few of the more specialist entries: SMB is the Windows file sharing protocol, ONTAP is NetApp’s storage operating system that many enterprises already run on premises, and Lustre is a file system built for high performance computing scratch space where thousands of nodes read and write in parallel.

NeedBetter choice
Shared Linux file system not anchored on S3EFS
Existing S3 data with file oriented workloadsS3 Files
High throughput read heavy S3 access with limited mutationMountpoint for Amazon S3
Windows shares or SMBFSx for Windows File Server
ONTAP or NAS migrationFSx for NetApp ONTAP
HPC scratch or Lustre semanticsFSx for Lustre
On premises file gateway patternAWS Storage Gateway

2. What S3 Files actually is

S3 Files is a shared file system linked to an S3 bucket or bucket prefix. You create an S3 file system, create mount targets in your VPC, and mount it from compute using the S3 Files mount helper. The mounted path behaves like a file system, while the backing data continues to live in S3. AWS says the service supports NFS v4.1 and v4.2 protocols, including file system access semantics such as read after write consistency, file locking and POSIX permissions.

The core resources are simple but worth setting out clearly:

ResourceWhat it does
S3 bucket or prefixThe authoritative object store where your durable data lives
S3 file systemThe shared file system linked to the bucket or prefix
High performance storageThe low latency active data layer used for metadata and frequently accessed file contents
Mount targetA network endpoint in a VPC availability zone through which compute mounts the file system
Access pointAn application specific entry point that can enforce a POSIX identity and root directory
Synchronisation configurationRules that decide what data is imported to the file system and when it expires from the active layer

The clever part is the two tier model. S3 Files does not eagerly copy your whole bucket into the high performance layer, because that would be expensive and architecturally pointless. Instead, metadata and selected file data are loaded onto the high performance storage layer as you access directories and files, while the file system view of the whole bucket remains traversable regardless of what has actually been copied there. Small, latency sensitive files can be served from that layer, while large reads and data that is not resident there can be streamed directly from S3. AWS documents the default import threshold as 128 KiB and notes that large reads of 1 MiB or more are streamed directly from S3 even when the data also resides on the high performance storage layer.

Under the hood, S3 Files uses Amazon EFS and delivers roughly one millisecond of latency for active data, according to the launch blog. That detail matters because AWS is not simply exposing S3 through a thin file client. It is using a real managed file system layer for the hot working set, then routing other reads directly to S3 where S3 is stronger. For mixed workloads, this is the entire point of the design. Small, interactive, metadata heavy operations get file system behaviour, while large sequential reads still benefit from S3 throughput.

3. What S3 Files is not

S3 Files is not a magic way to pretend that object storage and file storage are the same thing. S3 still has a flat object model underneath everything, where directories remain prefixes and objects remain immutable, and renames or moves still have to be translated into object level operations whenever changes are synchronised back to S3. AWS explicitly warns that when you rename or move a file, S3 Files must write the data to a new object with the updated key and delete the original, and when you rename a directory it must repeat that copy and delete process for every object under the prefix.

It is also not a replacement for every AWS file service. EFS still makes sense when you want a general shared Linux file system that is not fundamentally anchored on S3 as the source of truth. FSx still makes sense for Windows file shares, ONTAP compatibility, OpenZFS, Lustre based HPC and NAS migrations where the required feature set is broader or more specialised. AWS itself positions FSx as the right choice for on premises NAS migrations, HPC and workloads that need the specific capabilities of FSx for NetApp ONTAP, OpenZFS, Windows File Server or Lustre.

It is not the same thing as Mountpoint for Amazon S3 either. Mountpoint is a high throughput open source client that lets Linux applications access S3 objects through file operations such as open and read, but AWS documents that Mountpoint cannot modify existing files or delete directories, and it does not support symbolic links or file locking. Mountpoint is excellent for large scale read heavy workloads that want S3 throughput through a local file interface, while S3 Files is the more complete shared file system option when mutation, collaboration, locking, POSIX permissions and synchronisation matter.

It is not a free lunch for small file churn. S3 Files has its own metering model. You pay for active data stored on the high performance layer, for file system access charges when reading from and writing to that layer, and for S3 requests involved in synchronisation. Every operation is metered as a read or write against file data or metadata, with minimum metered sizes for reads, writes and metadata operations. This is much better than copying an entire bucket into a file system, but it means noisy metadata heavy workloads, repeated permission changes and excessive tiny writes can still cost money.

It is worth making that concrete rather than leaving it as an abstract warning. File and metadata operations are metered with minimum sizes of 32 KiB for data and 4 KiB for metadata. A workload that performs ten million tiny metadata operations, for example a build tool that stats every file in a large repository on every run, is billed as if each of those ten million operations moved 4 KiB, which is roughly 40 GB of metered metadata traffic, regardless of how small the actual files are. That is not an enormous sum of money on its own, but it compounds quickly once you have several such workloads running on a schedule, and it is exactly the kind of cost that never shows up in a proof of concept because nobody runs a proof of concept ten million times.

It is not an archive access layer. Objects in Glacier Flexible Retrieval, Glacier Deep Archive and the archive tiers of S3 Intelligent Tiering cannot be accessed through S3 Files until they are restored using the S3 API. If your bucket is a long term archive, S3 Files will not turn cold objects into immediately mountable working files.

It is not full NFS with every optional feature available. Test the actual application, not just basic file operations such as touch, cat and ls, because the gaps below are easy to miss until something in your stack quietly depends on one of them.

Unsupported NFS featureWhy it matters
pNFSParallel NFS layout distribution is not available
Client delegation and callbacksClients cannot be granted exclusive local caching authority
Mandatory lockingOnly advisory locking semantics are supported
NFS ACLsAccess control is POSIX permissions only, not ACL based
Kerberos based securityNo Kerberos authentication for the NFS mount
The nconnect mount optionYou cannot multiplex a single mount over several TCP connections
Server side copyCopy operations are not offloaded server side
Sparse file operationsSparse file semantics are not preserved
Custom user defined extended attributesOnly the S3 Files status attribute is exposed, not arbitrary xattrs

None of this makes S3 Files weak. It simply means the protocol surface is deliberately narrower than a full NFS server, in exchange for the whole thing being managed and anchored on S3.

4. Why this is better than the old pattern

The old pattern was usually ugly. An application would read from S3, copy data to local disk, mutate files, upload the result and then clean up. In a batch job that was irritating but manageable. In an application running across many nodes, an ML training pipeline, a media pipeline or an agentic AI workflow, it became a whole architecture made of staging buckets, temporary disks, synchronisation jobs, lifecycle rules, retries, locking conventions and error recovery scripts.

S3 Files replaces customer built synchronisation glue with a managed, observable, asynchronous synchronisation contract, rather than removing synchronisation as a system property altogether. The application can use ordinary file operations while the data remains anchored in S3. The file system can be mounted by multiple compute resources, including EC2, ECS, EKS and Lambda, and it supports concurrent access from multiple compute resources with NFS close to open consistency, according to the AWS launch blog. That makes it a considerably better fit for collaborative workloads than a single node object mount or a custom loop that downloads, processes and uploads in sequence. The synchronisation still exists, and still has real limits, as sections 6 and 21 cover, but it is now something you can observe and alarm on rather than something you wrote yourself and hoped would hold.

The second improvement is that it avoids full data duplication. S3 Files copies only the active working set onto the high performance layer. AWS says small, frequently accessed files can be served from that layer at latencies from under a millisecond to a few milliseconds, while large reads are streamed directly from S3 at up to terabytes per second of aggregate throughput. That is exactly the split you want, low latency for interactive file work and high throughput for large object reads, without any requirement to preload an entire lake before you can use it.

The third improvement is operational. S3 Files publishes CloudWatch metrics for storage, inodes, pending exports, import failures, export failures, data reads and writes, metadata reads and writes, lost and found files, and client connections. The client also publishes connectivity metrics showing whether the NFS connection is accessible, whether the S3 bucket is accessible, and whether the S3 bucket is reachable from the client. That means synchronisation lag, permission errors, conflicts and client connectivity problems can be monitored directly instead of inferred after the fact from application failures.

5. Why you should care

You should care if you have ever written code whose real job was not business logic but moving files between object storage and file systems. You should care if your Python library, shell tool, ML framework, video tool, indexer or enterprise application expects paths rather than S3 URIs. You should care if your organisation has built a data lake in S3 but still runs tools that behave as though the world is a directory tree.

The biggest immediate use case is agentic AI. Agents like files. They explore directories, read text, write temporary outputs, call command line tools, patch files and chain tools that were never designed for object APIs. A bucket full of documents, logs, prompts, code, configuration or generated artefacts becomes far more useful once many agents and jobs can walk it as a file system without every tool needing to be rewritten against the S3 API.

The second use case is ML training and data preparation. Many training loops still assume a directory of files, particularly for image, text, media and small record datasets. S3 Files lets you keep the dataset in S3 while preloading or caching the hot subset that benefits from lower latency. AWS gives an ML example where repeated reads of thousands of small files can use a higher size threshold, such as 10 MiB, with directory first import and a short expiration window once the training job has completed.

The third use case is enterprise modernisation. Many old applications do not need to become cloud native in one heroic rewrite. They need a compatible file surface over cloud native storage so they can move first and be rewritten later, if the rewrite is still justified once they have moved. S3 Files gives you that middle ground. Retain S3 as the system of record, but give file oriented code a path based interface into it.

The fourth use case is shared operational tooling. Think of incident bundles, reconciliation files, generated reports, support artefacts, fraud evidence packages, model outputs or risk exports. S3 is often where this data should live, but humans and tools generally want to browse and mutate a file tree. S3 Files gives those workflows a cleaner interface without forcing every participant to speak the S3 API directly.

6. The architecture in more detail

A simple S3 Files architecture has an S3 bucket, an S3 file system scoped to the bucket or a prefix, one mount target per availability zone where compute runs, and one or more compute clients mounting the file system. All of this sits inside a VPC (Virtual Private Cloud, your own logically isolated network within AWS), which is why the mount target has a private address rather than a public one. When you mount from EC2, the mount helper uses the instance profile credentials, starts the proxy process, establishes a TLS encrypted connection to the mount target, and starts the mount watchdog process. AWS documents the mount helper as defining a new file system type called s3files, compatible with the standard Linux mount command.

The network path matters more than it might first appear. A mount target is created in a subnet and has security groups attached. The compute security group must be able to send outbound TCP 2049 to the mount target security group, and the mount target security group must allow inbound TCP 2049 from the compute security group. If this is wrong, the failure looks like an NFS timeout rather than an S3 error, which is exactly the kind of thing that eats an afternoon if you have not documented it in advance.

The IAM model has two sides. First, the S3 Files service needs an IAM role it can assume to read from and write to the bucket, manage synchronisation, and manage the EventBridge rule (AWS’s event routing service, used here purely as internal plumbing to detect changes on the S3 side) it uses to detect S3 side changes. AWS documents the service principal as elasticfilesystem.amazonaws.com, with a trust policy scoped by source account and S3 Files source ARN. Second, the compute resource mounting the file system needs permissions such as s3files:ClientMount, optionally s3files:ClientWrite, optionally root access, and direct S3 read permissions if you want the client side direct read optimisation to work.

Synchronisation is asynchronous, but predictably so, which means you can design around it rather than being surprised by it. Writes go to the high performance layer and are durable immediately, but S3 Files waits about sixty seconds to aggregate successive changes before exporting the result back to S3. AWS documents up to 800 exported files per second per file system and up to 2,700 MB per second of export throughput, while import from S3 can process up to 2,400 object changes per second per file system with up to 700 MB per second of import throughput.

It is worth being precise about what fsync() actually buys you here, since this is exactly the kind of detail that only surfaces once someone has already built something on top of a wrong assumption. Under NFS, a write normally lands in the client’s local cache first, and a synchronous operation such as fsync(), or closing a file after a non appending write, is what forces that cached data across the network as an NFS commit to the server, which in this case is the mount target sitting in front of the high performance storage layer. AWS’s own EFS documentation says that applications performing synchronous writes gain stronger read after write consistency guarantees than the default close to open model, and since the high performance layer is built on EFS, that guarantee holds there. What fsync() does not do is wait for, or trigger, the separate export step that copies the change back into S3. That export still runs on its own roughly sixty second batching cycle no matter how aggressively your application calls fsync(). So fsync() on S3 Files gives you the same thing it has always given you on NFS, a guarantee that the write left the client and landed on durable server side storage, but that server side storage is the high performance layer, not S3 itself. A successful fsync() is not proof that S3 has the data yet, and it is also not free to call constantly, since a commit is metered in the same way as any other write to the high performance layer.

That means S3 Files is genuinely useful, but it is not a synchronous S3 object write through layer. If your application writes a file and another process immediately calls the S3 copy command expecting to read the object through the S3 API, you need to account for the batching window, since the file is visible immediately if you read it back through the mounted file system, but only appears through the S3 API once it has actually been exported.

7. Configuration options that actually matter

The first configuration option is the file system scope. You can create a file system over a whole bucket or over a prefix. Prefix scoping is usually the better choice because it reduces blast radius, reduces accidental traversal, reduces rename risk and makes access control simpler. AWS explicitly recommends scoping a file system to the smallest prefix your workload needs, partly because directory renames over large prefixes can become expensive and slow at the S3 synchronisation layer.

The second option is the import rule. An import data rule has a prefix, a trigger and a size threshold. The trigger can be set to import file data when a directory is first accessed, or only when a file is first read. The size threshold controls which files have data imported onto the high performance layer, with a default of 131,072 bytes, or 128 KiB. You can set it to zero for metadata only browsing, or raise it for workloads that repeatedly read larger small files.

The third option is the expiration rule. This controls when unused data is removed from the high performance layer once it has not been read for a period of time. AWS documents this window as ranging from one to 365 days, with a default of thirty days. Short lived batch jobs may want one to seven days. Developer file shares may want thirty days. Reused ML datasets may want thirty to ninety days.

The fourth option is access points. Access points let you enforce a POSIX user identity and a root directory for a particular application. This is extremely useful in shared environments because you can avoid every application mounting the entire file system under its own arbitrary Linux user context. The AWS CLI supports creating access points with a POSIX user and a root directory, including creation permissions for the access point root path.

The fifth option is encryption. Your S3 bucket must use SSE-S3 or SSE-KMS, and S3 Files encrypts data at rest using AWS KMS (Key Management Service, AWS’s managed service for creating and controlling the encryption keys behind your data) keys, with AWS owned keys by default or customer managed keys if you specify them. For a production environment, customer managed KMS keys are usually preferable because they make ownership, auditing and key lifecycle decisions explicit rather than implicit. AWS also states that S3 Files encrypts data in transit using TLS between the client and the mount target.

The sixth option is network reachability to S3. Direct read routing depends on the client being able to read from the linked S3 bucket. AWS says that if the bucket reachable metric is zero, you should verify that the compute resource has network access to S3, for example through a VPC endpoint or a NAT gateway. In private subnets, you should normally use an S3 gateway VPC endpoint rather than paying NAT gateway costs for S3 traffic that does not need to leave the AWS network in the first place.

8. Basic AWS CLI setup script

The script below creates a minimal S3 Files environment for a test account. It assumes you already have a VPC, subnet and EC2 security group in place. It uses SSE-S3 for simplicity, creates a bucket with versioning enabled, creates the S3 Files service role, creates a mount target security group, creates the S3 file system, applies a synchronisation configuration, and creates a mount target.

Run this in a non production AWS account first. The syntax follows the documented aws s3files command family, including create-file-system, create-mount-target and put-synchronization-configuration.

#!/usr/bin/env bash
set -euo pipefail
# ----------------------------
# User configuration
# ----------------------------
export AWS_PAGER=""
REGION="${REGION:-eu-west-1}"
NAME="${NAME:-s3files-demo}"
BUCKET="${BUCKET:-$NAME-$(date +%s)-$RANDOM}"
# Existing network resources
VPC_ID="${VPC_ID:?Set VPC_ID, for example vpc-0123456789abcdef0}"
SUBNET_ID="${SUBNET_ID:?Set SUBNET_ID in the same VPC, for example subnet-0123456789abcdef0}"
EC2_SG_ID="${EC2_SG_ID:?Set EC2_SG_ID for the EC2 instance that will mount S3 Files}"
ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
BUCKET_ARN="arn:aws:s3:::$BUCKET"
echo "Region:     $REGION"
echo "Account:    $ACCOUNT_ID"
echo "Bucket:     $BUCKET"
echo "VPC:        $VPC_ID"
echo "Subnet:     $SUBNET_ID"
echo "EC2 SG:     $EC2_SG_ID"
# ----------------------------
# 1. Create S3 bucket
# ----------------------------
if [[ "$REGION" == "us-east-1" ]]; then
  aws s3api create-bucket \
    --bucket "$BUCKET" \
    --region "$REGION"
else
  aws s3api create-bucket \
    --bucket "$BUCKET" \
    --region "$REGION" \
    --create-bucket-configuration LocationConstraint="$REGION"
fi
aws s3api put-bucket-versioning \
  --bucket "$BUCKET" \
  --versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
  --bucket "$BUCKET" \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "AES256"
        }
      }
    ]
  }'
aws s3api put-public-access-block \
  --bucket "$BUCKET" \
  --public-access-block-configuration '{
    "BlockPublicAcls": true,
    "IgnorePublicAcls": true,
    "BlockPublicPolicy": true,
    "RestrictPublicBuckets": true
  }'
mkdir -p /tmp/s3files-seed/data /tmp/s3files-seed/config
echo "hello from S3 before mounting" > /tmp/s3files-seed/config/hello.txt
dd if=/dev/urandom of=/tmp/s3files-seed/data/1mb.bin bs=1M count=1 status=none
aws s3 sync /tmp/s3files-seed "s3://$BUCKET/"
# ----------------------------
# 2. Create IAM role assumed by S3 Files
# ----------------------------
ROLE_NAME="$NAME-service-role"
cat > /tmp/s3files-trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3FilesAssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": "elasticfilesystem.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "$ACCOUNT_ID"
        },
        "ArnLike": {
          "aws:SourceArn": "arn:aws:s3files:$REGION:$ACCOUNT_ID:file-system/*"
        }
      }
    }
  ]
}
EOF
aws iam create-role \
  --role-name "$ROLE_NAME" \
  --assume-role-policy-document file:///tmp/s3files-trust-policy.json >/dev/null
cat > /tmp/s3files-service-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "S3BucketPermissions",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:ListBucketVersions"
      ],
      "Resource": "$BUCKET_ARN",
      "Condition": {
        "StringEquals": {
          "aws:ResourceAccount": "$ACCOUNT_ID"
        }
      }
    },
    {
      "Sid": "S3ObjectPermissions",
      "Effect": "Allow",
      "Action": [
        "s3:AbortMultipartUpload",
        "s3:DeleteObject*",
        "s3:GetObject*",
        "s3:List*",
        "s3:PutObject*"
      ],
      "Resource": "$BUCKET_ARN/*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceAccount": "$ACCOUNT_ID"
        }
      }
    },
    {
      "Sid": "EventBridgeManage",
      "Effect": "Allow",
      "Action": [
        "events:DeleteRule",
        "events:DisableRule",
        "events:EnableRule",
        "events:PutRule",
        "events:PutTargets",
        "events:RemoveTargets"
      ],
      "Condition": {
        "StringEquals": {
          "events:ManagedBy": "elasticfilesystem.amazonaws.com"
        }
      },
      "Resource": [
        "arn:aws:events:*:*:rule/DO-NOT-DELETE-S3-Files*"
      ]
    },
    {
      "Sid": "EventBridgeRead",
      "Effect": "Allow",
      "Action": [
        "events:DescribeRule",
        "events:ListRuleNamesByTarget",
        "events:ListRules",
        "events:ListTargetsByRule"
      ],
      "Resource": [
        "arn:aws:events:*:*:rule/*"
      ]
    }
  ]
}
EOF
aws iam put-role-policy \
  --role-name "$ROLE_NAME" \
  --policy-name "$NAME-service-policy" \
  --policy-document file:///tmp/s3files-service-policy.json
ROLE_ARN="arn:aws:iam::$ACCOUNT_ID:role/$ROLE_NAME"
sleep 15
# ----------------------------
# 3. Create security group for the mount target
# ----------------------------
MT_SG_ID="$(aws ec2 create-security-group \
  --region "$REGION" \
  --group-name "$NAME-mount-target-sg" \
  --description "S3 Files mount target security group" \
  --vpc-id "$VPC_ID" \
  --query GroupId \
  --output text)"
aws ec2 authorize-security-group-ingress \
  --region "$REGION" \
  --group-id "$MT_SG_ID" \
  --protocol tcp \
  --port 2049 \
  --source-group "$EC2_SG_ID"
echo "Created mount target security group: $MT_SG_ID"
# ----------------------------
# 4. Create the S3 file system
# ----------------------------
FS_ID="$(aws s3files create-file-system \
  --region "$REGION" \
  --bucket "$BUCKET_ARN" \
  --role-arn "$ROLE_ARN" \
  --tags key=Name,value="$NAME" key=Environment,value=demo \
  --query fileSystemId \
  --output text)"
echo "Created file system: $FS_ID"
for i in {1..60}; do
  STATUS="$(aws s3files get-file-system \
    --region "$REGION" \
    --file-system-id "$FS_ID" \
    --query status \
    --output text)"
  echo "File system status: $STATUS"
  [[ "$STATUS" == "available" || "$STATUS" == "AVAILABLE" ]] && break
  sleep 10
done
# ----------------------------
# 5. Tune synchronisation
# ----------------------------
aws s3files put-synchronization-configuration \
  --region "$REGION" \
  --file-system-id "$FS_ID" \
  --import-data-rules prefix="",trigger=ON_DIRECTORY_FIRST_ACCESS,sizeLessThan=131072 \
  --expiration-data-rules daysAfterLastAccess=30
# ----------------------------
# 6. Create mount target in the EC2 instance AZ
# ----------------------------
MT_ID="$(aws s3files create-mount-target \
  --region "$REGION" \
  --file-system-id "$FS_ID" \
  --subnet-id "$SUBNET_ID" \
  --security-groups "$MT_SG_ID" \
  --query mountTargetId \
  --output text)"
echo "Created mount target: $MT_ID"
cat > "$NAME.outputs" <<EOF
REGION=$REGION
BUCKET=$BUCKET
FILE_SYSTEM_ID=$FS_ID
MOUNT_TARGET_ID=$MT_ID
MOUNT_TARGET_SG_ID=$MT_SG_ID
SERVICE_ROLE_ARN=$ROLE_ARN
EOF
cat "$NAME.outputs"
echo "Now attach the right client permissions to your EC2 instance role, install amazon-efs-utils 3.0.0 or later, and mount with:"
echo "sudo mkdir -p /mnt/s3files"
echo "sudo mount -t s3files $FS_ID:/ /mnt/s3files"

9. EC2 client setup and mount script

On the EC2 instance, the client needs amazon-efs-utils version 3.0.0 or later. AWS documents amazon-efs-utils as the S3 Files client package, and says the client includes the mount helper as well as CloudWatch mount status monitoring support. It also documents that botocore is needed for the mount helper to interact with AWS services, including CloudWatch monitoring.

Your EC2 instance role should have either the managed policy for S3 Files client full access for a write test, or a more restricted policy with the required permissions. For direct read optimisation, the instance role also needs read access to the linked bucket, since without it the mount helper cannot read directly from S3 and all reads instead go through the file system layer.

#!/usr/bin/env bash
set -euo pipefail
FS_ID="${FS_ID:?Set FS_ID, for example fs-0123456789abcdef0}"
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
sudo yum -y install amazon-efs-utils jq attr || true
if ! command -v mount.s3files >/dev/null 2>&1; then
  sudo apt-get update || true
  sudo apt-get install -y amazon-efs-utils jq attr || true
fi
if ! command -v mount.s3files >/dev/null 2>&1; then
  echo "mount.s3files not found. Install amazon-efs-utils 3.0.0 or later for your distribution."
  exit 1
fi
sudo mkdir -p "$MOUNT_POINT"
sudo mount -t s3files "$FS_ID:/" "$MOUNT_POINT"
findmnt "$MOUNT_POINT"
df -hT "$MOUNT_POINT"
echo "Mounted $FS_ID at $MOUNT_POINT"

For automatic mounting after reboot, add an fstab entry using the _netdev option. AWS explicitly warns that this option should be used for automatic mounting because network file systems must initialise after networking is available, and missing it can leave the instance unresponsive on boot. It is also worth adding nofail, since that lets the instance complete its boot even if the file system happens to be unreachable at mount time, rather than hanging the boot sequence on a dependency that may be temporarily degraded.

FS_ID="${FS_ID:?Set FS_ID}"
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
sudo mkdir -p "$MOUNT_POINT"
grep -q "$FS_ID:/" /etc/fstab || echo "$FS_ID:/ $MOUNT_POINT s3files _netdev,nofail 0 0" | sudo tee -a /etc/fstab
sudo systemctl daemon-reload
sudo mount -a
findmnt "$MOUNT_POINT"

10. Access point setup for application isolation

An access point lets you mount a constrained view of the file system with a forced POSIX identity and an optional root directory. That is the safer production pattern for multiple tenants or multiple applications sharing a file system. Rather than allowing every workload to mount the whole file system under whatever user context the process happens to run as, you create one access point per workload and mount through it. AWS documents access points as application specific entry points that can enforce user identities and permissions for requests made through them.

#!/usr/bin/env bash
set -euo pipefail
REGION="${REGION:-eu-west-1}"
FS_ID="${FS_ID:?Set FS_ID}"
AP_ID="$(aws s3files create-access-point \
  --region "$REGION" \
  --file-system-id "$FS_ID" \
  --posix-user uid=1000,gid=1000 \
  --root-directory path=/app,creationPermissions="{ownerUid=1000,ownerGid=1000,permissions=0750}" \
  --tags key=Name,value=s3files-demo-app \
  --query accessPointId \
  --output text)"
echo "Access point: $AP_ID"
sudo mkdir -p /mnt/s3files-app
sudo mount -t s3files -o accesspoint="$AP_ID" "$FS_ID" /mnt/s3files-app
findmnt /mnt/s3files-app

11. Basic functional test script

This test proves four things: that data uploaded directly to S3 can be seen through the mount, that data written through the mount eventually appears in S3, that ordinary file operations work through the mounted file system, and that metadata and synchronisation are not instantaneous in both directions, which means tests must poll rather than assume synchronous object visibility.

#!/usr/bin/env bash
set -euo pipefail
BUCKET="${BUCKET:?Set BUCKET}"
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
TEST_ID="$(date +%Y%m%d%H%M%S)"
S3_KEY="tests/$TEST_ID/from-s3.txt"
FS_FILE="$MOUNT_POINT/tests/$TEST_ID/from-fs.txt"
echo "Test 1: write to S3, read from file system"
aws s3api put-object \
  --bucket "$BUCKET" \
  --key "$S3_KEY" \
  --body <(echo "written directly to S3 at $TEST_ID") >/dev/null
mkdir -p "$MOUNT_POINT/tests/$TEST_ID"
echo "Polling for the S3 created object to appear in the mounted file system..."
for i in {1..60}; do
  if [[ -f "$MOUNT_POINT/$S3_KEY" ]]; then
    cat "$MOUNT_POINT/$S3_KEY"
    break
  fi
  sleep 2
done
if [[ ! -f "$MOUNT_POINT/$S3_KEY" ]]; then
  echo "Object did not appear in the file system within the polling window."
  exit 1
fi
echo "Test 2: write to file system, read from S3"
echo "written through S3 Files at $TEST_ID" > "$FS_FILE"
sync
echo "File exists immediately through the file system:"
ls -l "$FS_FILE"
cat "$FS_FILE"
echo "Polling for the file system created object to appear in S3..."
for i in {1..90}; do
  if aws s3api head-object --bucket "$BUCKET" --key "tests/$TEST_ID/from-fs.txt" >/dev/null 2>&1; then
    aws s3 cp "s3://$BUCKET/tests/$TEST_ID/from-fs.txt" -
    break
  fi
  sleep 2
done
if ! aws s3api head-object --bucket "$BUCKET" --key "tests/$TEST_ID/from-fs.txt" >/dev/null 2>&1; then
  echo "File did not export to S3 within the polling window."
  exit 1
fi
echo "Test 3: rename and verify the eventual S3 key change"
mv "$FS_FILE" "$MOUNT_POINT/tests/$TEST_ID/from-fs-renamed.txt"
for i in {1..90}; do
  if aws s3api head-object --bucket "$BUCKET" --key "tests/$TEST_ID/from-fs-renamed.txt" >/dev/null 2>&1; then
    echo "Renamed key exists in S3."
    break
  fi
  sleep 2
done
echo "Test complete"

12. Performance and behaviour test scripts

Do not treat any of these as benchmarks suitable for vendor comparison. Treat them instead as behaviour tests that tell you whether your workload is doing lots of metadata operations, lots of tiny writes, large sequential reads, repeated small file reads, or a mix that does not match any of the neat categories in the documentation. That matters because S3 Files uses minimum metering sizes for file and metadata operations, while direct S3 streaming is favoured for large reads. AWS documents a 32 KiB minimum for file reads and writes, a 4 KiB minimum for metadata operations, and direct S3 streaming for large reads. The five scripts below cover general behaviour, write then immediate read latency, filename search, a full create, read and delete lifecycle, and sustained throughput under concurrency.

12.1 General behaviour benchmark

This is the broad sweep. It creates 10,000 small files, lists the directory, rereads the small files repeatedly, writes and reads a large file sequentially, and renames the whole tree, timing each phase.

#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
TEST_DIR="$MOUNT_POINT/bench/$(date +%Y%m%d%H%M%S)"
mkdir -p "$TEST_DIR"
echo "Benchmark directory: $TEST_DIR"
echo "Metadata: create 10,000 small files"
time bash -c '
for i in $(seq 1 10000); do
  printf "hello-%05d\n" "$i" > "'"$TEST_DIR"'/file-$i.txt"
done
'
echo "Metadata: list directory"
time ls "$TEST_DIR" >/dev/null
echo "Small file repeated read"
time bash -c '
for round in $(seq 1 5); do
  for f in "'"$TEST_DIR"'"/file-*.txt; do
    cat "$f" >/dev/null
  done
done
'
echo "Large sequential write"
time dd if=/dev/zero of="$TEST_DIR/large-512mb.bin" bs=8M count=64 conv=fdatasync status=progress
echo "Large sequential read"
time dd if="$TEST_DIR/large-512mb.bin" of=/dev/null bs=8M status=progress
echo "Rename a 10,000 file directory locally"
time mv "$TEST_DIR" "${TEST_DIR}-renamed"
echo "Done. Now watch PendingExports and DataWriteBytes in CloudWatch."

12.2 Write then immediate read latency

This script isolates a specific question that the general sweep hides inside averages. How long does it take to read a file back immediately after writing it, before anything has had a chance to settle. That matters for any workflow where one step writes a file and the very next step reads it, which is exactly the shape of most agent tool chains and many CI pipelines. The script writes a file, immediately reads it back, and records the gap for each of a few hundred files so you get a distribution rather than a single lucky number.

#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
TEST_DIR="$MOUNT_POINT/bench/write-then-read-$(date +%Y%m%d%H%M%S)"
COUNT="${COUNT:-300}"
mkdir -p "$TEST_DIR"
RESULTS="$TEST_DIR/../write-then-read-results.csv"
echo "file,write_ms,read_ms" > "$RESULTS"
for i in $(seq 1 "$COUNT"); do
  FILE="$TEST_DIR/wr-$i.txt"
  START_WRITE=$(date +%s%3N)
  printf "payload for file %d written at %s\n" "$i" "$(date -u)" > "$FILE"
  END_WRITE=$(date +%s%3N)
  START_READ=$(date +%s%3N)
  cat "$FILE" >/dev/null
  END_READ=$(date +%s%3N)
  echo "$i,$((END_WRITE - START_WRITE)),$((END_READ - START_READ))" >> "$RESULTS"
done
echo "Results written to $RESULTS"
echo "Summary (milliseconds):"
awk -F, 'NR>1 {w+=$2; r+=$3; n++} END {printf "avg write: %.2f, avg read: %.2f, samples: %d\n", w/n, r/n, n}' "$RESULTS"
sort -t, -k2 -n "$RESULTS" | tail -n 5
echo "Slowest write then read pairs shown above. A long tail here usually means cold directory or cold file overhead, not a systemic problem."

12.3 Filename search performance

Agents and build tools spend a surprising amount of time searching for files by name or pattern rather than reading file contents, so it is worth testing that in isolation. This script builds a moderately deep directory tree, then times a few common search patterns against it, including a full recursive listing, a find by name pattern, and a grep across file contents. Run it once against a warm tree and once against a freshly created one to see the cold path cost described in section 21.

#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
TEST_DIR="$MOUNT_POINT/bench/search-$(date +%Y%m%d%H%M%S)"
DEPTH="${DEPTH:-5}"
FILES_PER_DIR="${FILES_PER_DIR:-50}"
echo "Building test tree under $TEST_DIR (depth $DEPTH, $FILES_PER_DIR files per directory)"
CURRENT="$TEST_DIR"
for d in $(seq 1 "$DEPTH"); do
  CURRENT="$CURRENT/level-$d"
  mkdir -p "$CURRENT"
  for f in $(seq 1 "$FILES_PER_DIR"); do
    echo "content $d $f" > "$CURRENT/item-$f.txt"
  done
done
echo
echo "Recursive listing (find)"
time find "$TEST_DIR" -type f >/dev/null
echo
echo "Name pattern search (find -name)"
time find "$TEST_DIR" -type f -name "item-25.txt" >/dev/null
echo
echo "Content search across the tree (grep -r)"
time grep -rl "level-3" "$TEST_DIR" >/dev/null
echo
echo "Done. Rerun this script with an existing TEST_DIR pointed at an already warm tree to compare against the cold run above."

12.4 Create, read and delete lifecycle

Some of the most common workloads on S3 Files are genuinely ephemeral. A temporary output file from an agent, a Lambda scratch file, a CI artefact that only needs to survive one step. This script measures the full lifecycle of that pattern rather than just the create step, because the delete and the metadata cleanup that follows it are not free either, and they interact with the export window described in section 21.

#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
TEST_DIR="$MOUNT_POINT/bench/lifecycle-$(date +%Y%m%d%H%M%S)"
COUNT="${COUNT:-500}"
mkdir -p "$TEST_DIR"
RESULTS="$TEST_DIR/../lifecycle-results.csv"
echo "file,create_ms,read_ms,delete_ms" > "$RESULTS"
for i in $(seq 1 "$COUNT"); do
  FILE="$TEST_DIR/lc-$i.txt"
  T0=$(date +%s%3N)
  echo "ephemeral payload $i" > "$FILE"
  T1=$(date +%s%3N)
  cat "$FILE" >/dev/null
  T2=$(date +%s%3N)
  rm -f "$FILE"
  T3=$(date +%s%3N)
  echo "$i,$((T1 - T0)),$((T2 - T1)),$((T3 - T2))" >> "$RESULTS"
done
echo "Results written to $RESULTS"
awk -F, 'NR>1 {c+=$2; r+=$3; d+=$4; n++} END {printf "avg create: %.2f ms, avg read: %.2f ms, avg delete: %.2f ms, samples: %d\n", c/n, r/n, d/n, n}' "$RESULTS"
echo "If delete latency is high relative to create and read, check PendingExports and ExportFailures, since a delete still has to be reconciled back to S3 like any other change."

12.5 Sustained throughput under concurrency

A single dd process tells you very little about real throughput, because most workloads that care about throughput are running several readers or writers at once rather than one. This script runs a configurable number of parallel writers followed by a configurable number of parallel readers against separate large files, and reports aggregate throughput for each phase.

#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
TEST_DIR="$MOUNT_POINT/bench/throughput-$(date +%Y%m%d%H%M%S)"
PARALLEL="${PARALLEL:-8}"
FILE_SIZE_MB="${FILE_SIZE_MB:-256}"
mkdir -p "$TEST_DIR"
echo "Test directory: $TEST_DIR"
echo "Parallel streams: $PARALLEL, file size: ${FILE_SIZE_MB}MB each"
echo
echo "Parallel sequential writes"
START=$(date +%s)
for i in $(seq 1 "$PARALLEL"); do
  dd if=/dev/zero of="$TEST_DIR/stream-$i.bin" bs=8M count=$((FILE_SIZE_MB / 8)) conv=fdatasync status=none &
done
wait
END=$(date +%s)
ELAPSED=$((END - START))
TOTAL_MB=$((PARALLEL * FILE_SIZE_MB))
echo "Wrote ${TOTAL_MB}MB across $PARALLEL streams in ${ELAPSED}s"
if [[ "$ELAPSED" -gt 0 ]]; then
  echo "Aggregate write throughput: $((TOTAL_MB / ELAPSED)) MB/s"
fi
echo
echo "Parallel sequential reads"
START=$(date +%s)
for i in $(seq 1 "$PARALLEL"); do
  dd if="$TEST_DIR/stream-$i.bin" of=/dev/null bs=8M status=none &
done
wait
END=$(date +%s)
ELAPSED=$((END - START))
echo "Read ${TOTAL_MB}MB across $PARALLEL streams in ${ELAPSED}s"
if [[ "$ELAPSED" -gt 0 ]]; then
  echo "Aggregate read throughput: $((TOTAL_MB / ELAPSED)) MB/s"
fi
echo
echo "Compare this against a single stream run of the same total size to see how much the mount benefits from concurrency."
echo "Also watch DataReadBytes and DataWriteBytes in CloudWatch during this run to cross-check the client side numbers against the service side ones."

13. Synchronisation configuration examples

The default configuration is sensible for a general shared file tree. Import metadata and small file data on first directory access, then expire unused high performance data after thirty days. You should change it when your workload is obviously not general in this way. AWS supports up to ten import data rules per file system, and when multiple rules match a file, the most specific prefix wins.

13.1 Agentic browsing workload

Use this when agents need to discover many files but rarely reread the same content. It imports metadata, but does not import file data onto the high performance layer.

aws s3files put-synchronization-configuration \
  --region "$REGION" \
  --file-system-id "$FS_ID" \
  --import-data-rules prefix="",trigger=ON_FILE_ACCESS,sizeLessThan=0 \
  --expiration-data-rules daysAfterLastAccess=1

13.2 ML small file training workload

Use this when training repeatedly reads small examples over multiple epochs. It imports files under 10 MiB when a directory is first accessed, then expires the active data once the training period has ended.

aws s3files put-synchronization-configuration \
  --region "$REGION" \
  --file-system-id "$FS_ID" \
  --import-data-rules prefix="",trigger=ON_DIRECTORY_FIRST_ACCESS,sizeLessThan=10485760 \
  --expiration-data-rules daysAfterLastAccess=3

13.3 Hot and cold prefixes

Use this when a bucket has hot configuration or feature files under one prefix and cold archive like data under another. The hot prefix gets aggressive import, while the cold prefix remains metadata first and cheap to browse.

aws s3files put-synchronization-configuration \
  --region "$REGION" \
  --file-system-id "$FS_ID" \
  --import-data-rules \
    prefix="",trigger=ON_FILE_ACCESS,sizeLessThan=0 \
    prefix="config/",trigger=ON_DIRECTORY_FIRST_ACCESS,sizeLessThan=1048576 \
    prefix="archive/",trigger=ON_FILE_ACCESS,sizeLessThan=0 \
  --expiration-data-rules daysAfterLastAccess=30

14. Operational monitoring model

You should monitor S3 Files as a synchronisation system, a file system, and a client mounted network dependency, because those are three genuinely different failure modes. If you only monitor EC2 disk usage, you will miss S3 export failures. If you only monitor S3 request errors, you will miss lost and found conflicts. If you only monitor application logs, you will discover mount failures only after your customers have already discovered them.

The minimum CloudWatch metrics worth alarming on are set out below.

MetricNamespaceAlarm conditionWhy it matters
ExportFailuresAWS/S3/FilesGreater than zeroFiles failed terminal export back to S3
ImportFailuresAWS/S3/FilesGreater than zeroS3 side objects failed import into the file system
PendingExportsAWS/S3/FilesGrowing for a sustained periodFile system writes are outpacing synchronisation
LostAndFoundFilesAWS/S3/FilesGreater than zeroConflicting writes occurred between S3 and the file system
StorageBytesAWS/S3/FilesUnexpected growthActive data or lost and found data is accumulating cost
InodesAWS/S3/FilesUnexpected growthMetadata footprint is growing
NFSConnectionAccessibleefs-utils/S3FilesEquals zeroClient cannot reach the file system mount
S3BucketAccessibleefs-utils/S3FilesEquals zeroClient lacks S3 permissions for direct reads
S3BucketReachableefs-utils/S3FilesEquals zeroClient cannot reach the bucket or prefix

AWS documents S3 Files metrics in the AWS/S3/Files namespace with FileSystemId as the dimension, and client connectivity metrics in the efs-utils/S3Files namespace emitted by amazon-efs-utils. Most S3 Files metrics are sent at one minute intervals, while storage metrics are sent every fifteen minutes.

15. CloudWatch alarm creation script

This script creates conservative alarms. Tune the thresholds for your own environment, and in production connect the alarms to an SNS topic or whatever notification path actually wakes the right team.

#!/usr/bin/env bash
set -euo pipefail
REGION="${REGION:-eu-west-1}"
FS_ID="${FS_ID:?Set FS_ID}"
SNS_TOPIC_ARN="${SNS_TOPIC_ARN:-}"
ALARM_ACTIONS=()
if [[ -n "$SNS_TOPIC_ARN" ]]; then
  ALARM_ACTIONS=(--alarm-actions "$SNS_TOPIC_ARN")
fi
put_alarm() {
  local name="$1"
  local metric="$2"
  local statistic="$3"
  local period="$4"
  local evals="$5"
  local threshold="$6"
  local comparison="$7"
  local description="$8"
  aws cloudwatch put-metric-alarm \
    --region "$REGION" \
    --alarm-name "$name" \
    --alarm-description "$description" \
    --namespace "AWS/S3/Files" \
    --metric-name "$metric" \
    --dimensions Name=FileSystemId,Value="$FS_ID" \
    --statistic "$statistic" \
    --period "$period" \
    --evaluation-periods "$evals" \
    --threshold "$threshold" \
    --comparison-operator "$comparison" \
    --treat-missing-data notBreaching \
    "${ALARM_ACTIONS[@]}"
}
put_alarm \
  "s3files-$FS_ID-export-failures" \
  "ExportFailures" "Sum" 60 1 0 "GreaterThanThreshold" \
  "S3 Files export failures detected. Files may not have reached S3."
put_alarm \
  "s3files-$FS_ID-import-failures" \
  "ImportFailures" "Sum" 60 1 0 "GreaterThanThreshold" \
  "S3 Files import failures detected. S3 objects may not be visible through the file system."
put_alarm \
  "s3files-$FS_ID-lost-and-found" \
  "LostAndFoundFiles" "Sum" 60 1 0 "GreaterThanThreshold" \
  "S3 Files lost and found contains conflict files."
put_alarm \
  "s3files-$FS_ID-pending-exports-high" \
  "PendingExports" "Sum" 60 15 1000 "GreaterThanThreshold" \
  "S3 Files pending exports have remained high for 15 minutes."
put_alarm \
  "s3files-$FS_ID-storage-50gb" \
  "StorageBytes" "Maximum" 900 4 53687091200 "GreaterThanThreshold" \
  "S3 Files high performance storage exceeds 50 GiB."
echo "Created S3 Files alarms for $FS_ID"

16. CloudWatch dashboard script

This creates a dashboard showing active storage, metadata footprint, pending synchronisation, failed synchronisation, throughput, metadata activity, conflicts and client count.

#!/usr/bin/env bash
set -euo pipefail
REGION="${REGION:-eu-west-1}"
FS_ID="${FS_ID:?Set FS_ID}"
DASHBOARD_NAME="${DASHBOARD_NAME:-s3files-$FS_ID}"
cat > /tmp/s3files-dashboard.json <<EOF
{
  "widgets": [
    {
      "type": "metric", "x": 0, "y": 0, "width": 12, "height": 6,
      "properties": {
        "region": "$REGION",
        "title": "S3 Files active storage and inodes",
        "view": "timeSeries",
        "stacked": false,
        "metrics": [
          [ "AWS/S3/Files", "StorageBytes", "FileSystemId", "$FS_ID", { "stat": "Maximum" } ],
          [ ".", "Inodes", ".", ".", { "stat": "Sum", "yAxis": "right" } ]
        ],
        "period": 900
      }
    },
    {
      "type": "metric", "x": 12, "y": 0, "width": 12, "height": 6,
      "properties": {
        "region": "$REGION",
        "title": "Synchronisation health",
        "view": "timeSeries",
        "metrics": [
          [ "AWS/S3/Files", "PendingExports", "FileSystemId", "$FS_ID", { "stat": "Sum" } ],
          [ ".", "ImportFailures", ".", ".", { "stat": "Sum" } ],
          [ ".", "ExportFailures", ".", ".", { "stat": "Sum" } ],
          [ ".", "LostAndFoundFiles", ".", ".", { "stat": "Sum" } ]
        ],
        "period": 60
      }
    },
    {
      "type": "metric", "x": 0, "y": 6, "width": 12, "height": 6,
      "properties": {
        "region": "$REGION",
        "title": "Data throughput",
        "view": "timeSeries",
        "metrics": [
          [ "AWS/S3/Files", "DataReadBytes", "FileSystemId", "$FS_ID", { "stat": "Sum" } ],
          [ ".", "DataWriteBytes", ".", ".", { "stat": "Sum" } ]
        ],
        "period": 60
      }
    },
    {
      "type": "metric", "x": 12, "y": 6, "width": 12, "height": 6,
      "properties": {
        "region": "$REGION",
        "title": "Metadata activity and clients",
        "view": "timeSeries",
        "metrics": [
          [ "AWS/S3/Files", "MetadataReadBytes", "FileSystemId", "$FS_ID", { "stat": "Sum" } ],
          [ ".", "MetadataWriteBytes", ".", ".", { "stat": "Sum" } ],
          [ ".", "ClientConnections", ".", ".", { "stat": "Sum", "yAxis": "right" } ]
        ],
        "period": 60
      }
    }
  ]
}
EOF
aws cloudwatch put-dashboard \
  --region "$REGION" \
  --dashboard-name "$DASHBOARD_NAME" \
  --dashboard-body file:///tmp/s3files-dashboard.json
echo "Created dashboard: $DASHBOARD_NAME"

17. Command line health report script

This script produces a quick operational report for one file system. It fetches recent CloudWatch data, checks whether failed synchronisation or lost and found files are present, shows pending exports, and reports whether active storage is growing.

#!/usr/bin/env bash
set -euo pipefail
REGION="${REGION:-eu-west-1}"
FS_ID="${FS_ID:?Set FS_ID}"
END="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
START="$(date -u -d '30 minutes ago' +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-30M +"%Y-%m-%dT%H:%M:%SZ")"
metric_sum() {
  local metric="$1"
  aws cloudwatch get-metric-statistics \
    --region "$REGION" \
    --namespace AWS/S3/Files \
    --metric-name "$metric" \
    --dimensions Name=FileSystemId,Value="$FS_ID" \
    --start-time "$START" \
    --end-time "$END" \
    --period 60 \
    --statistics Sum \
    --query 'Datapoints[].Sum' \
    --output text | awk '{s=0; for(i=1;i<=NF;i++) s+=$i; print s+0}'
}
metric_max() {
  local metric="$1"
  local period="${2:-60}"
  aws cloudwatch get-metric-statistics \
    --region "$REGION" \
    --namespace AWS/S3/Files \
    --metric-name "$metric" \
    --dimensions Name=FileSystemId,Value="$FS_ID" \
    --start-time "$START" \
    --end-time "$END" \
    --period "$period" \
    --statistics Maximum \
    --query 'Datapoints[].Maximum' \
    --output text | awk '{m=0; for(i=1;i<=NF;i++) if($i>m) m=$i; print m+0}'
}
echo "S3 Files health report"
echo "File system: $FS_ID"
echo "Region:      $REGION"
echo "Window:      $START to $END"
echo
EXPORT_FAILURES="$(metric_sum ExportFailures)"
IMPORT_FAILURES="$(metric_sum ImportFailures)"
PENDING_EXPORTS="$(metric_max PendingExports)"
LOST_AND_FOUND="$(metric_max LostAndFoundFiles)"
STORAGE_BYTES="$(metric_max StorageBytes 900)"
INODES="$(metric_max Inodes 900)"
DATA_READ="$(metric_sum DataReadBytes)"
DATA_WRITE="$(metric_sum DataWriteBytes)"
META_READ="$(metric_sum MetadataReadBytes)"
META_WRITE="$(metric_sum MetadataWriteBytes)"
printf "%-24s %s\n" "ExportFailures:" "$EXPORT_FAILURES"
printf "%-24s %s\n" "ImportFailures:" "$IMPORT_FAILURES"
printf "%-24s %s\n" "PendingExports max:" "$PENDING_EXPORTS"
printf "%-24s %s\n" "LostAndFoundFiles max:" "$LOST_AND_FOUND"
printf "%-24s %s bytes\n" "StorageBytes max:" "$STORAGE_BYTES"
printf "%-24s %s\n" "Inodes max:" "$INODES"
printf "%-24s %s bytes\n" "DataReadBytes sum:" "$DATA_READ"
printf "%-24s %s bytes\n" "DataWriteBytes sum:" "$DATA_WRITE"
printf "%-24s %s bytes\n" "MetadataReadBytes sum:" "$META_READ"
printf "%-24s %s bytes\n" "MetadataWriteBytes sum:" "$META_WRITE"
echo
STATUS="OK"
if [[ "$EXPORT_FAILURES" != "0" ]]; then
  echo "CRITICAL: Export failures detected."
  STATUS="CRITICAL"
fi
if [[ "$IMPORT_FAILURES" != "0" ]]; then
  echo "CRITICAL: Import failures detected."
  STATUS="CRITICAL"
fi
if [[ "$LOST_AND_FOUND" != "0" ]]; then
  echo "WARNING: Lost and found files detected. Resolve conflicts and delete unneeded files."
  [[ "$STATUS" == "OK" ]] && STATUS="WARNING"
fi
if (( ${PENDING_EXPORTS%.*} > 1000 )); then
  echo "WARNING: Pending exports are high. Synchronisation may be falling behind."
  [[ "$STATUS" == "OK" ]] && STATUS="WARNING"
fi
echo "Overall: $STATUS"

18. Client mount health script

CloudWatch is necessary, but local mount health still matters in its own right. This script is designed to run from cron, a systemd timer, SSM Run Command, or whatever node health agent you already operate. It checks the mount, confirms that the file system is writable if you allow writes, checks the watchdog process, and collects the most useful local log entries.

#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
EXPECT_WRITE="${EXPECT_WRITE:-true}"
echo "S3 Files local mount health"
echo "Mount point: $MOUNT_POINT"
echo
if ! mountpoint -q "$MOUNT_POINT"; then
  echo "CRITICAL: $MOUNT_POINT is not a mount point."
  exit 2
fi
findmnt "$MOUNT_POINT"
df -hT "$MOUNT_POINT"
echo
echo "Checking read access..."
if ! ls "$MOUNT_POINT" >/dev/null; then
  echo "CRITICAL: Cannot list mount point."
  exit 2
fi
if [[ "$EXPECT_WRITE" == "true" ]]; then
  echo "Checking write access..."
  TEST_FILE="$MOUNT_POINT/.s3files-health-$(hostname)-$(date +%s).txt"
  if echo "health check $(date -u)" > "$TEST_FILE"; then
    rm -f "$TEST_FILE"
    echo "Write check passed."
  else
    echo "CRITICAL: Write check failed."
    exit 2
  fi
fi
echo
echo "Checking watchdog or proxy processes..."
ps aux | grep -E 'amazon-efs-mount-watchdog|efs-proxy|stunnel' | grep -v grep || true
echo
echo "Recent mount logs:"
sudo tail -n 50 /var/log/amazon/efs/mount.log 2>/dev/null || echo "No mount.log found."
echo
echo "Health check complete."

19. Lost and found conflict diagnostic script

Conflicts happen when the same logical file is modified through the file system and directly through S3 before S3 Files has had the chance to synchronise the file system side change back to S3. AWS documents that S3 is treated as the source of truth in that scenario, and the conflicting file system version is moved into a lost and found directory named for the file system. Those files remain there indefinitely and continue to count towards file system storage costs until you delete them.

#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files}"
LOST_DIR="$(find "$MOUNT_POINT" -maxdepth 1 -type d -name '.s3files-lost+found-*' | head -n 1 || true)"
if [[ -z "$LOST_DIR" ]]; then
  echo "No S3 Files lost and found directory found."
  exit 0
fi
echo "Lost and found directory: $LOST_DIR"
echo
find "$LOST_DIR" -type f | while read -r f; do
  echo "File: $f"
  if command -v getfattr >/dev/null 2>&1; then
    getfattr -n "user.s3files.status;$(date -u +%s)" "$f" --only-values 2>/dev/null || true
  else
    echo "Install the attr package to inspect S3 Files extended attributes."
  fi
  echo
done

20. Export failure diagnostic script

When a file written through the file system does not appear in S3 after the expected export window, inspect the file status using the S3 Files extended attribute. AWS documents getfattr -n "user.s3files.status;<timestamp>" as the way to retrieve the latest export status, and possible export errors include access denied, bucket not found, user metadata too large, file size over the S3 limit, an inaccessible KMS key, role assumption failure, path too long, and archived object.

#!/usr/bin/env bash
set -euo pipefail
FILE="${1:?Usage: ./s3files-export-status.sh /mnt/s3files/path/to/file}"
if ! command -v getfattr >/dev/null 2>&1; then
  echo "Install attr first, for example: sudo yum install -y attr"
  exit 1
fi
echo "Inspecting S3 Files export status for: $FILE"
getfattr -n "user.s3files.status;$(date -u +%s)" "$FILE" --only-values

21. Common gotchas

Before the individual gotchas, it is worth stating the underlying pattern once, because most of what follows is a variation on it. S3 Files is not S3 becoming POSIX. It is an active file system working layer sitting in front of S3, with S3 remaining the durable system of record underneath. Reads, writes, metadata, conflict handling, renames, lifecycle and cost all behave like a translation layer rather than S3 having magically become native NFS storage. Keep that mental model in view and most of the surprises below stop being surprising.

The first gotcha is forgetting S3 versioning, and the deeper version of this gotcha is underestimating what versioning then costs you. S3 Files requires versioning on the bucket so that it can synchronise changes between the file system and S3, and if you try to use an unversioned bucket, fix the bucket first rather than debugging the mount path, because the mount path is not the problem. The part that catches teams out later is that every edit exported from the file system becomes a new object version in S3, and every delete becomes a delete marker rather than a true removal. That is good for durability and for resolving conflicts cleanly, since it gives S3 Files something concrete to fall back on, but it is dangerous if you treat the mounted file system like disposable scratch space. A workload that repeatedly rewrites the same handful of files, for example a log file that gets appended to constantly or a cache file that gets regenerated on every run, will quietly grow the bucket’s object version history in a way that never shows up in the file system view itself. Pair S3 Files with a deliberate S3 lifecycle policy for noncurrent versions from day one, rather than discovering the growth in a bill three months in.

The second gotcha is the sixty second write export window, and the practical rule that follows from it. A file written through the mount is immediately visible through the file system, but it is not necessarily immediately visible through the S3 API. AWS documents that S3 Files waits approximately sixty seconds to aggregate successive changes before copying the file to S3, which reduces request cost and version churn but does mean the two views are not perfectly synchronous. The rule to design around is simple. If a producer writes through S3 Files and the consumer needs immediate consistency, the consumer should read through S3 Files too, not directly from S3. That lag is usually fine for batch workflows, but for event driven systems that fire on an S3 side trigger it becomes a trap that only shows up under load.

The third gotcha is treating a directory rename as cheap, and the sharper version of this gotcha is what happens if you rename while something is still writing. The rename is fast from the file system perspective, but S3 has no native directories and no atomic rename. When the change is exported to S3, every object under the prefix may require a copy and delete operation. A directory rename containing 100,000 files can take minutes to fully reflect in S3, according to the AWS performance documentation, and much larger trees can become expensive and operationally noisy in the process. The nastier detail is that during a large rename, both the old and new prefixes can be visible in the S3 bucket at once, and anything written directly to the old S3 prefix while the rename is still in flight may not get carried across to the new one. That looks exactly like a split brain incident to whoever is debugging it. Do not treat S3 Files as a safe way to reorganise a massive S3 prefix while live writes are still landing on it. If you need to move millions of keys, treat that as a migration project, not as a filesystem rename.

The fourth gotcha is conflict handling, which is predictable but not gentle. If the same logical file is changed through the file system and directly through S3 before synchronisation completes, S3 wins, and S3 Files moves the file system version into a lost and found directory named for the file system rather than trying to merge the two. Two practical traps follow from that. First, the lost and found directory can be invisible if you mount through an access point rooted below the file system’s actual root, so a workload that only ever sees its own access point may never notice conflicts piling up elsewhere. Second, files in lost and found stay there indefinitely and continue to count toward file system storage costs until someone deletes them. This deserves a scheduled check or an alarm of its own, because otherwise teams tend to discover lost conflict files only when a cost review or a data reconciliation incident forces the question.

The fifth gotcha is that permission changes are not free in the way they are on a local disk. POSIX owner, group and permission bits are stored as S3 object metadata, so running chmod, chown or chgrp causes S3 Files to export those metadata changes back to S3, and because S3 objects are immutable, each metadata change creates a new object version. A recursive permission fix over a large tree is therefore not the harmless, purely local operation it would be on ext4. It can generate a large number of new object versions, PUT requests and export work all at once. Be careful with recursive chmod or chown on large mounted prefixes, since on S3 Files metadata churn is object churn.

The sixth gotcha is cold first access latency. The first time you access a directory, S3 Files imports metadata for everything in it, and may also import data for small files depending on your synchronisation configuration. Subsequent access to that same directory is fast, but the first access can be materially slower, and that matters most for Lambda invocations, ECS tasks, batch jobs and short lived agents that only ever touch a directory once. The first job after a deployment can look slow while the second looks perfectly fine, which is confusing if you are not expecting it. Benchmark cold access and warm access separately rather than averaging them together, because S3 Files has a cold path cost that a naive benchmark will happily hide from you.

The seventh gotcha is specific to Lambda. The function needs to be attached to the same VPC path as the mount target, its security group needs to allow NFS on port 2049, and direct S3 reads for the intelligent read routing path are only supported for functions configured with 512 MB of memory or more. That memory threshold is easy to miss entirely. Teams mount the file system, test it with a small Lambda function, and then cannot work out why its behaviour differs from an EC2 client doing the same thing, when the actual answer is a memory setting that has nothing to do with S3 Files on the surface.

The eighth gotcha is IAM split brain. The S3 Files service role can be correct while the EC2 client role is wrong, or the EC2 client role can mount successfully but still be unable to use direct S3 read routing. If intelligent read routing is not working, AWS advises checking client connectivity metrics and verifying that the compute role has the required read permissions on the linked bucket.

The ninth gotcha is POSIX permissions in the more ordinary sense. IAM can allow the mount while POSIX permissions still deny file access underneath it. AWS documents that S3 Files enforces standard POSIX owner, group and other permissions on files and directories, and recommends access points when you need to enforce a specific user and group identity for all requests.

The tenth gotcha is archived objects. Glacier and deep archive objects are not readable through the file system until restored. This often surprises teams that point S3 Files at an older bucket and assume every key will behave like a hot file.

The eleventh gotcha is incompatible S3 keys. Empty path components, relative path components, null bytes, path components longer than 255 bytes, and object keys that do not map cleanly to POSIX paths are not supported through the file system. This is a real migration issue if your bucket was originally populated by systems that treated S3 as a loose key value store rather than as a path like object namespace.

The twelfth gotcha is modifying the generated EventBridge rule or service role. AWS recommends not modifying or deleting the S3 Files IAM role used for synchronisation, and not changing the associated EventBridge rule. If you break that plumbing, the file system can become stale because S3 side changes are no longer detected correctly.

The thirteenth gotcha is logging sensitive key names. AWS notes that efs-utils writes S3 object key names into logs under the EFS log directory, and recommends restricting access to that directory if object keys contain sensitive information. That is easy to miss because engineers often treat mount logs as harmless infrastructure logs rather than as something that might contain sensitive data.

The fourteenth gotcha is assuming the protocol is complete. S3 Files supports NFS v4.1 and v4.2 with documented exceptions, including no pNFS, no client delegation or callbacks, no mandatory locking, no NFS ACLs, no Kerberos based security and no deny share, alongside the narrower list already set out in the table in section 3. Say “NFS file access semantics with documented exceptions” rather than “full POSIX” or “full NFS” when you are describing this to a colleague or a vendor, because both of those phrases promise more than the service delivers and will eventually get quoted back at you.

The fifteenth gotcha is that the synchronisation ceiling is not a theoretical concern. AWS documents import processing of up to 2,400 object changes per second per file system and export processing of up to 800 files per second per file system. If your workload modifies files faster than export can keep up, PendingExports grows, and it should be read as a backlog metric rather than only a health metric. A rising backlog is telling you that your workload’s shape has outgrown the product’s shape, not merely that something is slightly behind. Sharding hot workloads across multiple file systems or multiple prefixes is a reasonable answer once you see that trend, and “millions of tiny output files” remains an anti pattern unless you have deliberately designed for it.

The danger with S3 Files is not that it is weak. The danger is that it is good enough to make people forget there is still a translation boundary underneath everything it does, that renames are still S3 key rewrites, that metadata changes are still object updates, that a writer using the S3 API directly can still race a writer using the mounted file system, that cold directories still need to be discovered before they are fast, and that pending exports still become a backlog if you outrun them. The product is powerful precisely because it hides so much of this by default, but production engineering is mostly the discipline of remembering what the abstraction is quietly hiding on your behalf.

22. Production design checklist

Use prefix scoped file systems unless you have a strong reason to expose the whole bucket. The smaller the scope, the easier the IAM, the lower the rename blast radius, and the cleaner the operational model overall.

Create mount targets in every availability zone where compute runs. AWS recommends creating a mount target in each availability zone you operate in, both to reduce cross zone data transfer costs and to improve performance. This also avoids confusing failures where the mount works on one node but not another, purely because that node landed in a zone without a local mount target.

Create one access point per application or trust boundary. Enforce POSIX identity through the access point, and use a root directory that maps to the application’s own prefix. That gives you a cleaner file system boundary than relying on every workload to voluntarily stay within its own lane.

Use customer managed KMS keys for regulated workloads, then test key disablement, key policy changes and service role assumption before production. AWS documents inaccessible KMS keys as a possible reason an encrypted file system can return NFS server errors or export failures.

Create CloudWatch alarms before allowing writes. The minimum alarms are export failures, import failures, lost and found files, and sustained pending exports. A production file system without those alarms is really a hidden data movement system with no visible synchronisation health contract.

Choose one primary writer for each path. Either the file system writes the active dataset and S3 side consumers read after export, or S3 side producers write and file system consumers read after import. When both sides write the same path at the same time, S3 wins, and the file system version is moved to lost and found.

Avoid giant mutable directories. If you expect a directory to be renamed, moved or reorganised, keep its object count bounded. S3 Files makes the file system side feel instant, but the S3 side still has to reconcile every object key underneath it.

Tune import rules by observed access patterns rather than by instinct. Start with metadata only for broad discovery, use 128 KiB for general development shares, and reach for larger thresholds only once repeated reads clearly justify the high performance storage cost.

Treat the mount as a dependency in its own right. The application should fail clearly when the mount is missing, degraded, read only or lagging. Add a startup check that validates the mount, performs a basic read, and optionally writes to a dedicated health check path.

23. When I would use S3 Files

I would use S3 Files for agent workspaces built over S3 backed document collections, especially where the agents use normal command line tools, Python libraries or other file based workflows. I would use it for ML training datasets made up of many small files where repeated reads make active set caching genuinely valuable. I would use it for document transformation pipelines where Lambda, ECS or EKS jobs need to mutate files without constantly downloading and uploading objects in a loop. I would use it for shared operational file trees where S3 must remain the durable system of record, but humans and tools still need a filesystem view over the top of it.

I would not use it as a blind replacement for every EFS workload. If the data does not belong in S3 in the first place, EFS may simply be the cleaner answer. I would not use it for HPC workloads that need Lustre semantics and performance patterns, because FSx for Lustre exists precisely for that reason. I would not use it for a bucket full of archived objects until the restore strategy is clear. And I would not use it for workloads that constantly rename massive directory trees, because S3’s object model will eventually present the bill for that decision.

24. The architectural significance

S3 Files matters because it softens a boundary that AWS spent twenty years teaching architects to respect. The old rule was simple: S3 is not a file system, and that rule earned its keep by stopping people from designing broken systems on top of a false assumption, since objects are immutable, directories are only prefixes, renames are never atomic, and POSIX assumptions leak badly over object storage the moment you let them.

The new rule is more interesting. S3 is still not a file system, but AWS can now place a real managed file system interface in front of selected S3 data and make that interface operationally credible for a wide range of workloads. That is a considerably more powerful idea than it first sounds. It means S3 can remain the durable centre of gravity while file oriented applications get a genuinely first class path into the same underlying data.

That is why S3 Files matters. It is not simply a storage feature. It is an architectural bridge between the object world and the file world, and a great deal of enterprise cloud architecture has spent years building that bridge badly, by hand, one glue script at a time.

25. Reaching S3 Files from outside AWS

Everything up to this point assumes your compute lives inside AWS. This last section is a deliberate detour for anyone running their own hardware, since it is really a different question from the rest of the article: not “how do I use S3 Files inside AWS”, but “can I reach it from outside AWS at all”. If that is not your situation, this is a reasonable place to stop.

The obvious next question, especially if you run anything on your own hardware, is whether S3 Files can be mounted from a device that is not itself inside AWS. Think a Raspberry Pi talking to another Raspberry Pi, or a Raspberry Pi talking to an EC2 instance, or any on premises machine that wants a genuine POSIX view of a bucket rather than a scripted sync. The honest answer has two parts, because the mount target itself is not internet facing, but there are documented ways to bridge into it.

A mount target is an elastic network interface inside your VPC, listening for NFS on TCP 2049, and by default it is only reachable from inside that VPC. This is exactly the same model EFS has always used, and AWS’s own on premises walkthrough for EFS confirms the pattern directly. Mounting an EFS file system from an on premises client requires either an AWS Direct Connect connection or a connection on an AWS VPN, and all of the VPC, the mount target and the file system must sit in the same AWS region. S3 Files sits on the same EFS based mount target architecture, so the same constraint applies to it. Your Raspberry Pi cannot simply resolve a public hostname and mount a file system the way it might reach an S3 bucket over HTTPS.

There are three practical ways to bridge that gap.

The first is AWS Client VPN or Site to Site VPN. You terminate a VPN connection into the VPC that holds your mount target, route your on premises network or your individual device into that VPC, and then mount exactly as you would from an EC2 instance, using the mount target’s private IP address. A community project that mounts S3 Files from macOS documents this as the recommended production pattern, noting that using AWS Client VPN to connect a client to the VPC is an AWS documented pattern for accessing EFS from on premises, and that it applies equally to S3 Files, which eliminates the need for an internet facing load balancer entirely. For a Raspberry Pi this means running a small VPN client on the Pi itself, which is well within what a Pi 5 can handle, and then treating the mount exactly as you would on any EC2 client.

The second is a Network Load Balancer fronting the mount target on port 2049, which gives you a single stable endpoint you can reach without a full VPN client on every device. The same project builds this for a proof of concept, with a mount target security group that only allows traffic from the load balancer’s security group, and recommends private subnets with a NAT Gateway, or better still a Gateway endpoint for S3, for anything beyond a proof of concept. This is a reasonable shape for a lab setup between two Raspberry Pis and an AWS account you control, but it does put an NFS listener on the internet, so if you go this route, lock the load balancer’s security group down to known source ranges and treat it the way you would treat any exposed NFS service, which is to say cautiously.

The third is a WireGuard or Tailscale style overlay network with one leg inside the VPC, for example a small EC2 instance or a container acting as a relay, and the other leg on your Raspberry Pi fleet. This is the closest match to how you already run andrewbaker.ninja, since your existing Pi already sits behind a Cloudflare Tunnel rather than a raw public listener. The same overlay pattern extends naturally to an S3 Files mount target, without opening anything new to the public internet.

A few things are worth weighing before you reach for any of these. Latency stops being an AWS internal concern and becomes an internet transit concern. Once you are mounting from outside AWS, latency is dominated by internet round trip time rather than by anything AWS controls, so picking the AWS region closest to your actual physical location matters more than it would for two resources that are already inside AWS. Cost also changes shape, since you are now paying for a VPN connection or a load balancer in addition to the usual S3 Files metering, and if you use a public NAT path instead of a VPC gateway endpoint for the underlying S3 traffic, that adds further cost for no real benefit.

Given all of that, I would only reach for a genuine S3 Files mount between a Raspberry Pi and AWS when you actually need POSIX semantics on both ends, meaning file locking, rename behaviour, or multiple writers touching the same tree concurrently. If what you actually want is a Raspberry Pi mirroring artefacts to or from a bucket on a schedule, plain aws s3 sync, rclone, or even Syncthing between two Pis will get you there with far less infrastructure and no VPN to keep alive. S3 Files earns its complexity when the file interface itself is the requirement, not when a simple copy would do the job.

For two Raspberry Pis with no AWS bucket in the picture at all, S3 Files is simply the wrong tool. That is a pure device to device problem, and your existing self hosted Docker stack, or something like Syncthing or Tailscale file sharing, solves it far more directly than routing two Pis through an AWS VPC just to talk to each other.