How Much Bigger Does pgcrypto Make Encrypted Column Data in YugabyteDB?

YugabyteDB supports the PostgreSQL pgcrypto extension for column-level encryption. But encrypting a column raises an interesting sizing question:

  • How much larger does the data become after it is encrypted?

The answer depends on several things:

  • ● The size of the original value.
  • ● The original data type.
  • ● Whether symmetric or public-key encryption is used.
  • ● Whether PGP compression is enabled.
  • ● Whether the encrypted result is stored as raw BYTEA, hexadecimal text, or ASCII-armored text.

The PGP encryption functions in pgcrypto return BYTEA. Internally, OpenPGP adds metadata around the original payload, including session-key information, random data similar to an IV, and integrity information. Public-key encryption additionally generates a random session key and encrypts that session key with the public key.

YugabyteDB includes pgcrypto as a pre-bundled extension, so it can be enabled with CREATE EXTENSION pgcrypto. YugabyteDB also documents both symmetric and public-key column-level encryption using this extension.

💡 The Important Part

There is no single “pgcrypto adds X%” answer. PGP encryption has some relatively fixed overhead, so small values can grow substantially as a percentage of their original size. As the plaintext becomes larger, that fixed overhead becomes a much smaller percentage of the total.

How Does pgcrypto Encryption Work?

For PGP encryption, pgcrypto supports both symmetric encryption and public-key encryption.

With symmetric encryption, a password is processed using a String-to-Key algorithm and used to derive encryption key material.

With public-key encryption, pgcrypto generates a random session key, encrypts the data using that session key, and then encrypts the session key using the supplied public key.

In both cases, the PGP format adds overhead beyond the original plaintext. PostgreSQL documents that the data may be compressed, is given a random prefix and integrity information, then encrypted and packaged into PGP packets.

Here are some of the approaches available through pgcrypto:

Method Function Output Size Consideration
Symmetric PGP pgp_sym_encrypt() BYTEA Adds the OpenPGP envelope and key-derivation metadata.
Public-Key PGP pgp_pub_encrypt() BYTEA Also includes an encrypted session-key packet, so small values can have additional overhead.
ASCII Armor armor() TEXT Adds Base64-style encoding, headers, formatting, and a checksum on top of the binary ciphertext.
Raw Cipher encrypt() / encrypt_iv() BYTEA Less PGP packaging, but key, IV, integrity, and other cryptographic details must be managed manually.

PostgreSQL specifically discourages using the raw encryption functions as a replacement for the PGP functions because they do not provide the PGP features such as integrity checking and require the caller to manage cryptographic parameters such as the IV.

🔐 Related YugabyteDB Tip

For a complete example of generating a public/private PGP key pair and using pgp_pub_encrypt() and pgp_pub_decrypt() in YSQL, see Column Level Public-Key Encryption in YSQL.

The existing tip walks through GPG key generation, public-key encryption, and private-key decryption.

Demo

For this experiment, we’ll use symmetric PGP encryption. That keeps the demo self-contained and lets us concentrate specifically on how the size of the encrypted payload changes.

Start a Single-Node YugabyteDB Cluster

This demonstration only needs a single YugabyteDB node.

				
					./bin/yugabyted start --advertise_address 127.0.0.1
				
			

Connect with ysqlsh:

				
					./bin/ysqlsh -h 127.0.0.1 -p 5433 -U yugabyte
				
			

A single-node yugabyted deployment uses RF=1 and is intended for local development and testing rather than a production deployment.

Create a database for the experiment:

				
					CREATE DATABASE pgcrypto_size_demo;
				
			

Connect to it:

				
					\c pgcrypto_size_demo
				
			

Enable pgcrypto:

				
					CREATE EXTENSION IF NOT EXISTS pgcrypto;
				
			

Create a Random-Data Generator

Rather than requiring an external data generator, we can build a simple helper function using pgcrypto itself.

The function generates chunks of cryptographically random bytes, converts them to printable hexadecimal characters, and keeps appending them until the requested length has been reached:

				
					CREATE OR REPLACE FUNCTION demo_random_text(p_length integer)
RETURNS text
LANGUAGE sql
VOLATILE
AS $$
  SELECT left(
           string_agg(encode(gen_random_bytes(32), 'hex'), ''),
           p_length
         )
    FROM generate_series(
           1,
           greatest(1, ceil(p_length / 64.0)::integer)
         );
$$;
				
			

Test it:

				
					SELECT demo_random_text(80);
				
			

You should see an 80-character random value.

Create the Test Table

We’ll test several different cases:

  • ● A very small TEXT value.
  • ● A large 16 KB TEXT value.
  • ● A native DATE.
  • ● An approximately 8 KB JSON document.
  • ● An approximately 16 KB JSONB document.

Each plaintext column has a corresponding encrypted BYTEA column.

				
					CREATE TABLE pgcrypto_size_test
(
    id                      integer PRIMARY KEY,

    text_small_original     text,
    text_small_encrypted    bytea,

    text_large_original     text,
    text_large_encrypted    bytea,

    date_original           date,
    date_encrypted          bytea,

    json_original           json,
    json_encrypted          bytea,

    jsonb_original          jsonb,
    jsonb_encrypted         bytea
);
				
			
Why BYTEA?

The PGP encryption functions return BYTEA. Storing the binary ciphertext directly avoids adding another text-encoding layer. Converting ciphertext to hexadecimal text approximately doubles the number of characters required, while ASCII armor adds Base64 and formatting overhead.

Create a Demo Encryption Key

For convenience, we’ll put the demonstration passphrase into a ysqlsh variable:

				
					\set demo_key 'YugabyteDB-pgcrypto-size-demo'
				
			

This is just for the experiment.

Production Key Management

Do not hard-code production encryption keys in application SQL, table definitions, stored procedures, or source code. Production applications should obtain encryption material from an appropriate secret-management or key-management system and should have a key-rotation strategy.

pgcrypto executes inside the database server. PostgreSQL therefore recommends using local or SSL/TLS connections and trusting the database and system administrators; otherwise encryption should be performed by the client application instead.

Generate and Encrypt 100 Rows

We’ll explicitly disable PGP compression for the first test so that we’re measuring the encryption overhead rather than mixing compression into the results.

AES-256 is used for this demonstration.

				
					WITH source_data AS
(
    SELECT
        g AS id,

        demo_random_text(32) AS text_small_original,

        demo_random_text(16384) AS text_large_original,

        DATE '2000-01-01'
            + floor(random() * 9500)::integer
            AS date_original,

        json_build_object(
            'record_id', g,
            'customer', demo_random_text(64),
            'description', demo_random_text(4096),
            'notes', demo_random_text(4096)
        ) AS json_original,

        jsonb_build_object(
            'record_id', g,
            'customer', demo_random_text(64),
            'payload', demo_random_text(16384),
            'active', (random() > 0.5)
        ) AS jsonb_original

    FROM generate_series(1, 100) AS g
)
INSERT INTO pgcrypto_size_test
(
    id,

    text_small_original,
    text_small_encrypted,

    text_large_original,
    text_large_encrypted,

    date_original,
    date_encrypted,

    json_original,
    json_encrypted,

    jsonb_original,
    jsonb_encrypted
)
SELECT
    id,

    text_small_original,
    pgp_sym_encrypt(
        text_small_original,
        :'demo_key',
        'cipher-algo=aes256,compress-algo=0'
    ),

    text_large_original,
    pgp_sym_encrypt(
        text_large_original,
        :'demo_key',
        'cipher-algo=aes256,compress-algo=0'
    ),

    date_original,
    pgp_sym_encrypt(
        date_original::text,
        :'demo_key',
        'cipher-algo=aes256,compress-algo=0'
    ),

    json_original,
    pgp_sym_encrypt(
        json_original::text,
        :'demo_key',
        'cipher-algo=aes256,compress-algo=0'
    ),

    jsonb_original,
    pgp_sym_encrypt(
        jsonb_original::text,
        :'demo_key',
        'cipher-algo=aes256,compress-algo=0'
    )

FROM source_data;
				
			

Verify that we inserted 100 rows:

				
					SELECT count(*) FROM pgcrypto_size_test;
				
			

Why Are DATE, JSON, and JSONB Cast to TEXT?

The PGP text encryption function accepts TEXT, not arbitrary PostgreSQL/YSQL data types.

That means:

				
					date_original::text
json_original::text
jsonb_original::text
				
			

are the values actually passed into pgp_sym_encrypt().

This creates an important distinction when measuring size.

For example, a native DATE is stored much more compactly internally than its printable value:

				
					2026-08-20
				
			

So there are actually two questions we can ask:

  • 1. How much larger is the ciphertext than the plaintext fed to pgcrypto?
  • 2. How much larger is the encrypted column than the original native YSQL value?

We’ll measure both.

Measure the Encryption Overhead

Run:

				
					WITH measurements AS
(
    SELECT
        1 AS sort_order,
        'TEXT - 32 characters' AS data_type,
        pg_column_size(text_small_original) AS source_column_bytes,
        octet_length(text_small_original) AS plaintext_input_bytes,
        pg_column_size(text_small_encrypted) AS encrypted_column_bytes,
        octet_length(text_small_encrypted) AS ciphertext_bytes
    FROM pgcrypto_size_test

    UNION ALL

    SELECT
        2,
        'TEXT - 16 KB',
        pg_column_size(text_large_original),
        octet_length(text_large_original),
        pg_column_size(text_large_encrypted),
        octet_length(text_large_encrypted)
    FROM pgcrypto_size_test

    UNION ALL

    SELECT
        3,
        'DATE',
        pg_column_size(date_original),
        octet_length(date_original::text),
        pg_column_size(date_encrypted),
        octet_length(date_encrypted)
    FROM pgcrypto_size_test

    UNION ALL

    SELECT
        4,
        'JSON - approximately 8 KB',
        pg_column_size(json_original),
        octet_length(json_original::text),
        pg_column_size(json_encrypted),
        octet_length(json_encrypted)
    FROM pgcrypto_size_test

    UNION ALL

    SELECT
        5,
        'JSONB - approximately 16 KB',
        pg_column_size(jsonb_original),
        octet_length(jsonb_original::text),
        pg_column_size(jsonb_encrypted),
        octet_length(jsonb_encrypted)
    FROM pgcrypto_size_test
)
SELECT
    data_type,

    round(avg(source_column_bytes), 1)
        AS avg_source_column_bytes,

    round(avg(plaintext_input_bytes), 1)
        AS avg_plaintext_input_bytes,

    round(avg(ciphertext_bytes), 1)
        AS avg_ciphertext_bytes,

    round(
        avg(ciphertext_bytes - plaintext_input_bytes),
        1
    ) AS avg_pgp_added_bytes,

    round(
        100.0 *
        (
            avg(ciphertext_bytes) /
            NULLIF(avg(plaintext_input_bytes), 0)
            - 1
        ),
        2
    ) AS pgp_growth_pct,

    round(
        100.0 *
        (
            avg(encrypted_column_bytes) /
            NULLIF(avg(source_column_bytes), 0)
            - 1
        ),
        2
    ) AS column_growth_pct

FROM measurements
GROUP BY sort_order, data_type
ORDER BY sort_order;
				
			

Actual Example Results

Data Type Source Column Plaintext Input Ciphertext PGP Added PGP Growth Column Growth
TEXT (32 characters) 36 bytes 32 bytes 98 bytes 66 bytes 206.25% 183.33%
TEXT (16 KB) 16,388 bytes 16,384 bytes 16,452 bytes 68 bytes 0.42% 0.41%
DATE 4 bytes 10 bytes 76 bytes 66 bytes 660.00% 1900.00%
JSON (approximately 8 KB) 8,328.9 bytes 8,324.9 bytes 8,392.9 bytes 68 bytes 0.82% 0.82%
JSONB (approximately 16 KB) 16,528 bytes 16,512.3 bytes 16,580.3 bytes 68 bytes 0.41% 0.34%
The Big Finding: About 66–68 Bytes of PGP Overhead

With compression disabled, this test showed remarkably consistent PGP overhead. The encrypted payload was only about 66–68 bytes larger than the plaintext supplied to pgcrypto. What changes dramatically is the percentage: adding 66 bytes to a 32-byte string is significant, while adding 68 bytes to a 16 KB value is almost negligible.

The results show why there isn’t a single percentage that can be used to estimate the storage impact of pgcrypto.

For the 32-character TEXT value, the ciphertext grew by more than 200% compared with the plaintext supplied to pgcrypto.

For the 16 KB TEXT value, however, the ciphertext increased by only 0.42%.

The same effect can be seen with the larger JSON and JSONB values.

The approximately 8 KB JSON value grew by only 0.82%, while the approximately 16 KB JSONB value grew by only 0.41% when comparing the ciphertext with the serialized plaintext.

In other words, the OpenPGP overhead becomes proportionally less important as the size of the value being encrypted increases.

Why Does DATE Grow So Much?

The DATE result is particularly interesting:

  • ● Native DATE column: 4 bytes
  • ● Plaintext sent to pgcrypto: 10 bytes
  • ● Encrypted ciphertext: 76 bytes
  •  
  • ● PGP growth: 660%
  • ● Column growth: 1900%
Why Did DATE Grow by 1900%?

A YSQL DATE occupies only 4 bytes, but pgp_sym_encrypt() does not encrypt the native DATE representation directly. The value is first converted to its 10-byte textual representation, such as 2026-08-20. The resulting PGP ciphertext averaged 76 bytes. When that encrypted value is compared with the original 4-byte DATE column, the apparent column-size increase becomes approximately 1900%.

This is a good example of why percentage growth can be misleading for very small native data types.

The encrypted date is 19 times larger than the original native DATE, but we’re still talking about only:

  • 4 bytes → 76 bytes

The absolute difference is only 72 bytes.

PGP Growth vs. Column Growth

There are two different measurements in the results:

Measurement What It Tells Us
pgp_growth_pct How much larger the PGP ciphertext is than the serialized plaintext actually supplied to pgcrypto.
column_growth_pct How the YSQL representation of the encrypted BYTEA value compares with the original native column value.

For TEXT, these numbers are fairly close because the original value is already text.

For types such as DATE, however, they can be dramatically different because the native value must first be converted to text before it is passed to pgcrypto.

An Interesting JSONB Result

There is another subtle result worth pointing out.

For JSONB, we measured:

  • ● Native JSONB column: 16,528.0 bytes
  • ● Serialized plaintext: 16,512.3 bytes
  • ● Encrypted ciphertext: 16,580.3 bytes
  •  
  • ● PGP growth: 0.41%
  • ● Column growth: 0.34%

Notice that the column-growth percentage is actually slightly lower than the PGP-growth percentage.

That’s because JSONB has its own binary representation and structural metadata. In this particular dataset, the native JSONB representation was already slightly larger than the textual representation that was passed to pgcrypto.

Data Type Still Matters

Once a value is passed to pgp_sym_encrypt(), pgcrypto is primarily concerned with the bytes being encrypted. However, when comparing the encrypted column with the original column, the native YSQL representation also matters. A 4-byte DATE, TEXT value, JSON document, and JSONB document do not have identical native storage characteristics.

Don’t Confuse pg_column_size() with Actual YugabyteDB Disk Usage

The measurements above use functions such as:

				
					pg_column_size()
				
			

and:

				
					octet_length()
				
			

These are useful for understanding the relative size of the values being stored, but they should not be interpreted as the exact number of additional bytes that will appear in YugabyteDB SST files.

YugabyteDB ultimately stores YSQL data using DocDB and its LSM-based storage engine. Actual physical storage also includes factors such as:

  • ● DocDB’s internal representation.
  • ● Row and column metadata.
  • ● RocksDB/SST block structures.
  • ● Compression.
  • ● Compaction.
  • ● Replication factor.
  • ● Indexes.
  • ● Multiple versions of data that may temporarily exist in the LSM tree.

Encryption can also affect compression. Ciphertext is intentionally high-entropy data and generally does not compress as effectively as repetitive plaintext.

Capacity Planning

Use these SQL measurements to understand the logical expansion caused by column-level encryption, but don’t use them as an exact prediction of physical YugabyteDB disk consumption. For capacity planning, test representative application data at realistic scale and measure the resulting database storage.

What Did We Learn?

The results from this test are surprisingly straightforward.

With PGP compression disabled, pgp_sym_encrypt() added approximately the same amount of PGP overhead to every value we tested:

  • Approximately 66–68 bytes

What changed dramatically was how significant those additional bytes were relative to the original data.

Final Takeaway

In this 100-row YugabyteDB test using pgp_sym_encrypt(), AES-256, and PGP compression disabled, pgcrypto added only about 66–68 bytes of PGP overhead to the serialized plaintext payload.

  • ● A 32-byte TEXT value grew by about 206% at the PGP payload level.
  • ● A native DATE showed about 1900% column growth when comparing its 4-byte native representation with the encrypted BYTEA column. At the PGP payload level, the 10-byte serialized DATE grew to 76 bytes, or about 660%.
  • ● Approximately 8 KB of JSON grew by only 0.82%.
  • ● Approximately 16 KB of TEXT grew by only 0.42%.
  • ● Approximately 16 KB of JSONB grew by only 0.34% when comparing the encrypted column with the original native column.

The important lesson is that pgcrypto’s PGP overhead was relatively small in absolute bytes in this test, but the percentage can look enormous for very small values. For the larger TEXT, JSON, and JSONB values tested here, the PGP payload increase was less than 1%.

Have Fun!

Congrats to my wife! After 19 years as a nurse at one of Pittsburgh’s largest health networks, she is finally retiring!

This is the cake her boss got for her goodbye party at work.

Luckily for me, it was a little too sweet for her taste… which meant I got to eat most of the leftovers.

Retirement is already working out pretty well for me! 😂🎂