My current contract involves setting up a serverless processing pipeline in AWS, storing all the data in S3. During development of the system, we have been working with a bucket that does not have versioning or encryption enabled, but we have reached a point where I have asked if the bucket ought to be encrypted.

We are dealing with electrical meter data (electricity consumption), so it is not PII but it is still confidential. Server-side encryption (SSE) on S3 only really protects against an attack vector involving access to Amazon’s physical storage, which is vastly more challenging than this data would be worth; however, enabling SSE on a bucket is trivial, so the configuration cost is essentially zero. With this in mind, encrypting the bucket seems like a reasonable choice to make.

The system architect agreed with this assessment, with the caveat that he would like to be sure that having encryption enabled doesn’t degrade performance too far. I had a brief search of the web to find any other evaluations of performance impacts, and found

Since these didn’t supply any numbers, I figured: why not do some basic measurements myself?

All our data access is from within AWS (Lambda and Glue), so I needed to perform the measurements from there. With that in mind, I created this Lambda code:

import io
import time
import uuid

import boto3

MB = 2**20
BASE_MEMORY_REQUIRED = 80  # MB


def create_file(size_mb: int) -> io.BytesIO:
    """
    Create and return a file-like buffer of 'size' random megabytes.
    """
    buffer = io.BytesIO()
    with open('/dev/urandom', 'rb') as random_source:
        for _ in range(size_mb):
            buffer.write(random_source.read(1 * MB))
    buffer.seek(0)
    return buffer


def lambda_handler(event: dict, context):
    file_size_mb = event['file_size_mb']
    unencrypted_bucket = event['unencrypted_bucket']
    encrypted_bucket = event['encrypted_bucket']
    iterations = event['iterations']

    # Check that the lambda has been set up properly
    if int(context.memory_limit_in_mb) <= file_size_mb + BASE_MEMORY_REQUIRED:
        raise ValueError("Insufficient memory allocated")

    def upload(bucket):
        big_file.seek(0)
        s3.put_object(
            Bucket=bucket,
            Key=file_name,
            Body=big_file,
        )
        
    def download(bucket):
        obj = s3.get_object(
            Bucket=bucket,
            Key=file_name,
        )
        obj['Body'].read()
        
    s3 = boto3.client('s3')
    unencrypted_upload_times = []
    encrypted_upload_times = []
    unencrypted_download_times = []
    encrypted_download_times = []
    print(f"Starting {iterations} uploads and downloads")
    for i in range(iterations):
        big_file = create_file(file_size_mb)
        file_name = uuid.uuid4().hex

        # Upload the data and time the operation (twice),
        # then download the data and time it (twice)
        t0 = time.time()
        upload(encrypted_bucket)
        encrypted_upload_times.append(time.time() - t0)
        print(f"{i}: uploaded to encrypted bucket")

        t0 = time.time()
        upload(unencrypted_bucket)
        unencrypted_upload_times.append(time.time() - t0)
        print(f"{i}: uploaded to unencrypted bucket")
        # Try to avoid doubling up on in-memory storage of the large files
        big_file.truncate(0)

        t0 = time.time()
        download(encrypted_bucket)
        encrypted_download_times.append(time.time() - t0)
        print(f"{i}: downloaded from encrypted bucket")

        t0 = time.time()
        download(unencrypted_bucket)
        unencrypted_download_times.append(time.time() - t0)
        print(f"{i}: downloaded from unencrypted bucket")

    result = {
        "iterations": iterations,
        "size": file_size_mb,
        "encrypted_upload_times": encrypted_upload_times,
        "unencrypted_upload_times": unencrypted_upload_times,
        "encrypted_download_times": encrypted_download_times,
        "unencrypted_download_times": unencrypted_download_times,
    }
    print(result)
    return {
        'statusCode': 200,
        'body': result,
        'headers': {'Content-Type': 'application/json'}
    }

and triggered it directly from the Lambda console “test” utility with the payload

{
  "file_size_mb": 800,
  "unencrypted_bucket": "alex-test-unencrypted",
  "encrypted_bucket": "alex-test-encrypted",
  "iterations": 10
}

I only requested 10 iterations because even that took nearly ten minutes to run with the 800MB file size that I specified. So I called it 5 times to obtain 50 measurements. The results looked like this:

Upload (s) Download (s)
median: 11.7 11.6
Unencrypted: mean: 12.5 11.6
std. dev.: 2.9 0.4
median: 11.7 10.9
Encrypted: mean: 11.9 10.7
std. dev.: 0.8 0.5

Which suggests that upload times are largely unaffected by the encryption setting, and that download times are interestingly faster for encrypted files (at least for these files).

In conclusion, enabling encryption on the S3 bucket has no cost (direct financial or performance) so there is no reason not to do so. Note, however, that this measurement only used SSE-S3, not SSE-KMS. From some brief searching of the web, SSE-KMS can run into performance problems in terms of rate of files uploaded/downloaded (as opposed to rate of bytes uploaded/downloaded) because it requires S3 to make API calls to KMS to obtain the key for each operation; this can be largely mitigated by using a Bucket Key, but that’s well beyond the scope of this brief investigation.

Appendix - the raw time measurements (in seconds):

Encrypted upload Unencrypted upload Encrypted download Unencrypted download
11.6940746307373 11.6849784851074 10.9234869480133 11.7144949436188
11.6625287532806 11.6226992607117 11.4408304691315 11.6158993244171
11.7032141685486 11.7632384300232 10.8988852500916 11.7233171463013
12.1810274124146 11.6629092693329 10.9905335903168 12.8688085079193
11.6816043853760 11.6620612144470 10.8789787292480 11.6937589645386
11.7196235656738 11.6734302043915 10.9571630954742 11.6186110973358
11.6261374950409 11.6171290874481 9.2294242382050 11.2199978828430
12.1792688369751 11.6139698028564 10.9129040241241 11.6814973354340
11.6769218444824 11.6034467220306 10.8799097537994 11.7172236442566
11.6727154254913 11.7504541873932 10.8840353488922 11.6745781898499
11.6735568046570 11.6607069969177 10.9813044071198 11.6213848590851
11.6849887371063 11.6179919242859 10.8705642223358 11.6690998077393
12.0919978618622 11.6796560287476 9.0999648571014 11.3372871875763
11.6507017612457 11.6462154388428 10.9386730194092 11.7185146808624
11.5898842811584 11.6660833358765 10.9372851848602 11.6424448490143
11.6603825092316 11.6032352447510 9.1814439296722 11.1970849037170
11.6504487991333 20.8611717224121 10.9414134025574 11.5785925388336
11.5966084003448 11.6695580482483 11.0593311786652 11.7907378673553
11.6372516155243 11.6758489608765 11.0018763542175 11.7571446895599
11.8446044921875 11.6281900405884 10.9578194618225 9.5198469161987
11.7846033573151 11.8456094264984 9.2133951187133 10.9204108715057
11.6085646152496 11.6443903446198 10.7986392974854 11.5597372055054
11.7643485069275 11.6076791286469 10.8625876903534 11.5612387657166
11.7053418159485 11.6414282321930 10.8453776836395 11.6012265682220
11.6800470352173 11.6984360218048 10.8957929611206 11.5266029834747
11.6613471508026 11.6728124618530 10.8214366436005 11.6234931945801
11.6896800994873 22.1839056015015 10.8185594081879 11.6012597084045
11.7522304058075 11.6759369373322 10.9014334678650 11.5810294151306
11.6505093574524 11.6274704933167 10.8986401557922 11.5984861850739
11.6225833892822 11.6417267322540 10.8359308242798 11.5892403125763
11.7100932598114 11.5911505222321 10.8719291687012 11.5881307125092
11.6352610588074 11.6236982345581 10.8744866847992 11.5054509639740
12.3622174263000 11.5923130512238 10.8601109981537 11.4798181056976
11.6519224643707 11.6992461681366 10.8413724899292 11.4571144580841
11.9042441844940 11.7184102535248 11.0654361248016 11.7385985851288
11.8565964698792 11.6578516960144 10.9584598541260 11.5479769706726
12.9413495063782 11.6869413852692 10.8612637519836 11.6412565708160
11.6435585021973 11.7229723930359 10.8753590583801 11.5985469818115
11.6253650188446 11.6678795814514 10.8005053997040 11.5813326835632
17.4464807510376 11.6508176326752 10.8987245559692 11.5757186412811
11.6235389709473 11.6626203060150 10.7445497512817 11.5973939895630
11.7414813041687 11.6903252601624 9.2054750919342 11.2935853004456
11.7315373420715 25.7158513069153 11.2086730003357 11.6013922691345
11.6676566600800 11.6517312526703 10.7980945110321 11.5454034805298
11.7108561992645 19.0991778373718 10.8938765525818 11.5185081958771
11.7484521865845 11.6650342941284 10.9214606285095 11.5405542850494
12.2797617912293 11.6601886749268 10.8545019626617 11.5244343280792
11.6670072078705 11.5868990421295 10.8798396587372 11.5391335487366
11.7142891883850 11.6777644157410 10.8001849651337 11.5678985118866
11.6488909721375 11.6711585521698 10.8583946228027 11.5788042545319