How do Neon and Aurora Serverless v2 actually bill for compute?

Both services charge for provisioned compute capacity over time, not for the queries you run. A query costs money only indirectly: it keeps compute running, pushes autoscaling to a higher capacity level, or generates storage I/O. The rest of this article depends on that distinction.

Neon measures compute in compute units (CU). A Neon compute endpoint autoscales between a minimum and maximum CU setting, and you pay for CU multiplied by the time the endpoint is active. Storage and data transfer are billed separately. If an endpoint suspends after it goes idle, compute charges stop, but storage charges continue.

Aurora Serverless v2 measures compute in Aurora Capacity Units (ACU), billed per second at whatever capacity the instance is running at. Storage is billed separately. I/O billing depends on the cluster's storage configuration: Aurora Standard charges per I/O request, and Aurora I/O-Optimized drops I/O charges in exchange for higher compute and storage rates.

Dimension Neon Aurora Serverless v2
Compute unit CU ACU
Billed as CU × active time ACU × seconds running
Storage Separate charge Separate charge
I/O Not a separate line item Per request (Standard) or bundled (I/O-Optimized)
Transfer Separate charge Standard AWS data transfer rates

Each unit combines a fixed amount of memory with a proportional share of CPU. The two vendors define that ratio differently, so 1 CU and 1 ACU are not interchangeable. Before you compare costs, check each vendor's current documentation for the memory-per-unit ratio and the rate in your region.

What does a query really cost in serverless Postgres?

Neither vendor charges per query. You pay for capacity held over time, measured in unit-hours (Neon CUs, Aurora ACUs), whether or not that capacity is executing anything. The useful model is:

  • Monthly compute cost = Σ over each billing interval of (provisioned units × interval length in hours × unit-hour rate)
  • Cost per query = monthly compute cost ÷ queries served that month

A query's own execution time barely matters. What matters is how much capacity the platform held before, during, and after it ran.

units
  4 |        ┌──┐
  3 |        │  └─┐          ← scale-down lag: capacity held
  2 |        │    └──┐          after the burst has ended
  1 |        │       └─────┐
0.5 |────────┘             └────────  ← idle floor (min capacity)
    +──────────────────────────────── time
     idle    burst  lag     idle

Under steady load, the area under this curve is spread across many queries, so each one carries a small share. Under sparse load, one short burst triggers a scale-up, then the lag holds that capacity for minutes after the work is done. The idle floor accrues charges between bursts. A handful of queries ends up absorbing the entire area.

Illustrative estimate (assumed numbers, not a benchmark or measured result). Every input below is assumed: a rate of $0.15 per unit-hour, a 720-hour month, and the capacity levels and query counts shown. Substitute your vendor's actual regional rate and your own traffic.

  • Steady (illustrative, assumed inputs): assume 2 units held for 720 h = 1,440 unit-hours = $216 at the assumed rate. Spread across an assumed 10,000,000 queries, that is about $0.0000216 per query.
  • Sparse (illustrative, assumed inputs): assume a 0.5-unit floor held for 720 h = 360 unit-hours = $54 at the assumed rate. Spread across an assumed 100,000 queries, that is $0.00054 per query. Under these assumptions, that is roughly 25× more per query for the same SQL.
 ┌──────────────────┐     ┌─────────────────────────────┐
 │ Query workload   │────▶│ Autoscaler holds capacity   │
 │ (arrivals/time)  │     │ floor, burst, scale-down lag│
 └────────┬─────────┘     └──────────────┬──────────────┘
          │                              │ units held per interval
          │                              ▼
          │               ┌─────────────────────────────┐
          │               │ Σ units × hours × rate      │
          │               │ = monthly compute cost      │
          │               └──────────────┬──────────────┘
          │                              │ ÷ queries served
          │ queries served               ▼
          │               ┌─────────────────────────────┐
          └──────────────▶│ cost per query              │
                          └──────────────┬──────────────┘
                                         │
               ┌─────────────────────────┴─────────────────┐
               ▼                                           ▼
  ┌─────────────────────────┐                 ┌─────────────────────────┐
  │ Steady: 2 units × 720 h │                 │ Sparse: 0.5 unit × 720 h│
  │ = 1,440 unit-h = $216   │                 │ = 360 unit-h = $54      │
  │ ÷ 10,000,000 queries    │                 │ ÷ 100,000 queries       │
  │ ≈ $0.0000216 / query    │                 │ ≈ $0.00054 / query      │
  └─────────────────────────┘                 └─────────────────────────┘
  area spread over many queries               floor + lag on few queries

The autoscaler's capacity curve (idle floor, burst, scale-down lag) determines the unit-hours billed. Dividing that cost by queries served gives cost per query. The figures in the diagram are the section's illustrative estimate, using an assumed $0.15 per unit-hour and assumed workloads, not measured results.

Which workloads benefit from serverless Postgres pricing? An illustrative estimate

Illustrative estimate (assumed numbers, not a benchmark). Assume $0.15 per compute-unit-hour, a 720-hour month, and 15 minutes (0.25 h) of scale-down lag after each burst. The spreadsheet formula is:

monthly_cost = rate × (active_h × avg_units + wakes × lag_h × peak_units + idle_h × floor_units)

Workload (assumed) Active Lag Idle floor Unit-hours Cost
(a) Dev DB, 3 h/day at 1 unit, 2 wakes/day, scales to zero 90 15 0 105 $15.75
(a′) Same, but 0.5-unit floor, no scale-to-zero 90 15 307.5 412.5 $61.88
(b) Steady API, 4 units around the clock 2,880 0 0 2,880 $432.00
(c) Nightly batch, 30 min at 8 units, 0.5-unit floor 120 60 348.75 528.75 $79.31

In (a), allowing the database to scale to zero cuts the bill by roughly 4×. That is the case serverless pricing is designed for. In (c), the actual batch work accounts for only 23% of the unit-hours. The rest comes from the 8-unit lag tail after each run and the floor held during roughly 700 idle hours. Lowering the floor or shortening the lag saves more money than optimizing the batch queries would.

Workload (b) never goes idle, so it gets no benefit from elastic billing. Suppose a provisioned instance of comparable size costs $0.40/h (also an assumed figure). That works out to $288 per month, and a reserved commitment would lower it further. For steady, high-utilization traffic, a provisioned or reserved instance is the main alternative, and it is often the cheaper one.

Serverless fits development databases, preview branches, low-traffic tenants, and bursty jobs, as long as you can tolerate cold starts and can keep the floor near zero. Enter your own rates, floor, and lag into the formula before you decide.

from decimal import Decimal, ROUND_HALF_UP

RATE = Decimal("0.15")        # assumed $ per compute-unit-hour
MONTH_H = 720                  # hours in the modeled month
PROVISIONED_HOURLY = Decimal("0.40")  # assumed comparable provisioned instance

def monthly(active_h, avg_units, wakes, lag_h, peak_units, floor_units):
    lag_total_h = wakes * lag_h                 # billed tail after each burst
    idle_h = max(MONTH_H - active_h - lag_total_h, 0)
    active_u = active_h * avg_units
    lag_u = lag_total_h * peak_units            # lag is billed at peak size
    idle_u = idle_h * floor_units               # floor held while idle
    units = active_u + lag_u + idle_u
    cost = (RATE * Decimal(str(units))).quantize(Decimal("0.01"), ROUND_HALF_UP)
    return active_u, lag_u, idle_u, units, cost

workloads = {
    "(a)  dev, scale to zero": (3 * 30, 1, 2 * 30, 0.25, 1, 0),
    "(a') dev, 0.5 floor":     (3 * 30, 1, 2 * 30, 0.25, 1, 0.5),
    "(b)  steady API":         (MONTH_H, 4, 0, 0.25, 4, 4),
    "(c)  nightly batch":      (0.5 * 30, 8, 30, 0.25, 8, 0.5),
}

for name, args in workloads.items():
    a, l, i, u, cost = monthly(*args)
    share = 100 * a / u if u else 0
    print(f"{name:26} active={a:g} lag={l:g} idle={i:g} "
          f"unit_h={u:g} cost=${cost} real_work={share:.0f}%")

print(f"provisioned alternative for (b): ${PROVISIONED_HOURLY * MONTH_H}")

A plug-in-your-own-rates Python version of the section's formula that reproduces the illustrative table, showing how much of each bill comes from active work, scale-down lag, and the idle floor, and comparing (b) to an assumed provisioned rate.

When should you avoid scale-to-zero and low capacity limits?

Neon suspends a compute after a period of inactivity and resumes it when a new connection arrives. The first connection after a suspend pays for the compute starting up. On Aurora Serverless v2, the minimum capacity setting is your idle floor. Whether it can pause to zero depends on engine version and cluster configuration, so check what your cluster supports before you build a cost model around zero.

Scale-to-zero becomes a problem when the first request after idle carries an SLO. A resume adds latency at exactly the moment a user is waiting. If your driver or pooler uses a short connect timeout, a cold start can show up as a connection error rather than a slow request. Measure resume time against your timeouts before you enable it in production.

Capacity limits involve similar trade-offs:

  • Low max capacity. A traffic spike that would have scaled the database instead waits in line. Latency rises and connections pile up, so the cap protects your bill by making requests slower.
  • High min capacity. You pay a higher floor, but buffers stay warm and the first queries after a quiet period don't start cold.
  • Memory shrinking on Aurora. When capacity drops, the buffer cache shrinks with it and can evict your working set. The queries that follow read from storage, and if your cluster is billed per I/O, those reads cost money.

Aggressive suspend settings, fewer replicas, and short retention also weaken operations. Failover gets slower or disappears, your point-in-time recovery window narrows, and less history is available when you need to debug an incident.

What keeps a serverless Postgres database awake?

An idle-savings estimate assumes the database actually goes idle. In practice, many clients keep it busy without doing useful work. Common culprits include application pools that hold minimum connections open, load-balancer health checks that run SELECT 1 every few seconds, ORM keepalive pings, monitoring agents that poll statistics views, and logical-decoding consumers such as CDC pipelines that keep a walsender process streaming. Any recurring query resets the inactivity timer. On some platforms, an open connection alone is enough to block pausing.

Audit before you trust the numbers. Query pg_stat_activity grouped by usename, application_name, client_addr, and state, and compare backend_start with state_change. This shows sessions that have lived for days and wake up briefly at regular intervals. Filter on backend_type to separate client backends from walsenders. Next, check pg_replication_slots for active and active_pid. An active logical slot means a consumer is attached. An inactive slot is a different problem: it still retains WAL, which drives up storage.

Server-side transaction-mode pooling reduces the number of long-lived backends, because the pooler releases a server connection after each transaction. The trade-off is that session state no longer persists between transactions. Depending on the pooler and its configuration, the following can break:

  • session-level SET
  • session advisory locks
  • temporary tables
  • LISTEN
  • protocol-level prepared statements

Before you switch, audit your drivers and ORMs for these features. Route sessions that need them to a direct, unpooled endpoint.

-- 1. Who holds connections open? Group client backends and walsenders by origin.
SELECT backend_type, usename, application_name, client_addr, state,
       count(*)                   AS sessions,
       min(backend_start)         AS oldest_session,
       max(state_change)          AS last_activity
FROM pg_stat_activity
WHERE backend_type IN ('client backend', 'walsender')
  AND pid <> pg_backend_pid()          -- exclude this audit session
GROUP BY 1, 2, 3, 4, 5
ORDER BY sessions DESC;

-- 2. Long-lived sessions that woke up recently: likely keepalives,
--    health checks, or pool minimums. Last query text hints at the source.
SELECT pid, usename, application_name, client_addr,
       now() - backend_start AS session_age,
       now() - state_change  AS idle_for,
       left(query, 60)       AS last_query
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND state = 'idle'
  AND backend_start < now() - interval '1 hour'
  AND state_change  > now() - interval '5 minutes'
ORDER BY session_age DESC;

-- 3. Replication slots: active = a consumer is attached and streaming;
--    inactive = no wake-ups, but WAL is still retained (storage cost).
--    Run on the primary; pg_current_wal_lsn() fails during recovery.
SELECT slot_name, slot_type, plugin, database,
       active, active_pid,
       pg_size_pretty(
         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
       ) AS retained_wal
FROM pg_replication_slots
ORDER BY active DESC, slot_name;

Three catalog queries that group open sessions by origin, flag long-lived connections that wake on a schedule, and list replication slots that either keep the database streaming or retain WAL.

How do inefficient queries raise I/O and autoscaling costs?

On Aurora, the cluster storage configuration decides how bad queries are billed. Under Aurora Standard, read and write I/O operations are metered and billed per request. A sequential scan over a large table, or a sort that spills to temporary files, shows up as I/O spend. Under Aurora I/O-Optimized, there is no per-request I/O charge. You pay higher compute and storage rates instead, so heavy I/O is already priced in whether or not your queries need it. Neon bills compute time plus storage and has no per-I/O line item. Reads that miss the compute's local cache still have to fetch pages from remote storage, which keeps the compute busy and running longer.

Both platforms share a second cost: heavy queries push capacity up. Scale-down then lags behind, so you keep paying for elevated capacity after the query finishes. To model it, use this formula:

extra cost ≈ (peak capacity − baseline) × (query duration + scale-down lag) × unit price

To attribute that cost, run EXPLAIN (ANALYZE, BUFFERS) and look at three things:

  • shared read counts versus shared hit counts
  • Seq Scan nodes on large tables
  • Sort Method: external merge

Then line the timestamps up with the vendor's capacity metrics, such as Aurora's ServerlessDatabaseCapacity in CloudWatch or Neon's compute usage graphs. Metrics are sampled coarsely and billing data arrives late, so per-query attribution is an approximation, not an accounting.

Fixing a missing index cuts spend directly. It also cuts the hours engineers lose to explaining an unexplained bill spike after the fact.

-- 1. Capture the plan with buffer counts. Note the wall-clock time so you can
--    line it up with ServerlessDatabaseCapacity (Aurora) or compute usage (Neon).
SELECT now() AS run_started_at;

EXPLAIN (ANALYZE, BUFFERS)
SELECT customer_id, sum(amount) AS total
FROM orders
WHERE created_at >= now() - interval '7 days'
GROUP BY customer_id
ORDER BY total DESC
LIMIT 50;

-- What to look for in the output:
--   Seq Scan on orders            -> whole table read to find one week of rows
--   Buffers: shared hit=.. read=.. -> 'read' = pages not in shared buffers;
--                                    on Aurora Standard these feed billed read I/O,
--                                    on Neon a cache miss waits on remote storage
--   Sort Method: external merge   -> sort spilled to temp files (more I/O, longer run)

-- 2. Remove the full scan. CONCURRENTLY avoids blocking writes;
--    it cannot run inside a transaction block.
CREATE INDEX CONCURRENTLY IF NOT EXISTS orders_created_at_idx
    ON orders (created_at);

-- 3. If a sort still spills, raise memory for this session only and re-check.
SET work_mem = '64MB';

EXPLAIN (ANALYZE, BUFFERS)
SELECT customer_id, sum(amount) AS total
FROM orders
WHERE created_at >= now() - interval '7 days'
GROUP BY customer_id
ORDER BY total DESC
LIMIT 50;
-- Expect an Index Scan or Bitmap Heap Scan, far fewer 'read' buffers,
-- and 'Sort Method: quicksort' or 'top-N heapsort' instead of external merge.

-- Illustrative estimate (assumed inputs, not a measurement):
--   baseline 2 capacity units, peak 8, query 5 min + scale-down lag 10 min
--   extra = (8 - 2) x 0.25 h = 1.5 capacity-unit-hours x your unit price, per run

Reading EXPLAIN (ANALYZE, BUFFERS) before and after adding an index and adjusting work_mem shows the scan, buffer-read, and sort-spill signals that drive I/O and capacity cost, along with an illustrative estimate of the extra capacity each run costs.

How do you choose and tune Neon or Aurora Serverless v2 for your workload?

Compute is rarely the whole bill. On Aurora Serverless v2, each reader instance consumes its own ACUs, and readers in the highest failover priority tiers scale alongside the writer, so a replica added "for safety" can roughly double baseline compute. On Neon, every branch and read replica that runs queries gets its own compute endpoint with its own active time. Storage also grows with the history window. Neon's restore window and Aurora's backup retention both keep data you are paying for after you have stopped querying it. Traffic between application and database in different availability zones, and egress out of the region, is billed separately from database compute. Per-pull-request preview environments multiply all of these, so cap their lifetime and keep their history windows short.

A measurement-first checklist

Collect two weeks of metrics before changing settings:

Measurement What it tells you Setting to tune first
Active-time fraction Whether scale-to-zero saves anything Suspend timeout, min capacity
Peak-to-baseline ratio Whether autoscaling beats a fixed size Max capacity
Connection lifetime Whether idle sessions keep compute awake Pooling mode (transaction pooling)
Read/write I/O volume Whether I/O, not compute, dominates Indexes, replica placement

A database that is active most of the day with a flat load profile usually costs less on provisioned capacity. Serverless pays off when the database is often idle or has sharp, infrequent peaks.

These numbers become a baseline. When the bill moves, you compare against measured figures instead of starting a new investigation from scratch, which saves the engineering time spent on repeated cost reviews.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to serverless postgres cost, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Changing a shared variable is one edit and one review instead of editing every service's .env by hand.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.