Skip to content

Cheap Postgres Options

Background

Azure Database for PostgreSQL Flexible Server is deployed on dev and prod via infra/modules/postgres.bicep. The current prod server (dna-prd-portfolio-ne-pg) runs on Standard_D2s_v3 (GeneralPurpose, ~€44/mo). A planned downgrade to Standard_B1ms (Burstable, ~€6–12/mo) is blocked by CapacityNotAvailable in northeurope as of 2026-05-03 — deploy_postgres = false is temporarily set in parameters.prod.bicepparam.

This document evaluates all cheap Postgres options and makes a recommendation.


Options Compared

Option 1 — Azure Flexible Server Burstable B1ms (~€6–12/mo)

The cheapest Azure-native option. Already configured in postgres_sku = 'Standard_B1ms' in prod params — just blocked by capacity.

Property Value
Cost ~$12.41/mo compute + storage (West Europe)
vCores / RAM 1 vCore / 2 GiB
Managed Identity auth ✅ Full support (current setup)
Azure-native ✅ Same region, no cross-cloud latency
Availability ❌ Blocked by northeurope CapacityNotAvailable (transient)

Action: Retry az postgres flexible-server update --sku-name Standard_B1ms --tier Burstable periodically. Restore deploy_postgres = true once it succeeds.


Option 2 — Neon Serverless Postgres (Free → $19/mo)

Pure SaaS, runs on AWS Frankfurt for EU workloads. No Azure resource involved.

Property Value
Free tier 0.5 GB storage, 191 compute-hours/month, scale-to-zero
Paid (Launch) $19/mo — more compute hours, no project pausing
Managed Identity auth ❌ Password/connection string only
Cold start ~1–2s (Neon) + ~2s (ACA) = ~3–4s combined on dev
Cross-cloud latency ~5–20ms (AWS Frankfurt → Azure West Europe)
Azure Native Integration ~~Available~~ — retired 2025, use direct SaaS
DB branching ✅ Instant branches per PR

Catch: Migrating to Neon requires removing all Managed Identity auth code (ManagedIdentityCredential, IMDS token fetch, AAD admin registration) and replacing with a connection string stored in Key Vault. Significant regression in security posture.


Option 3 — Supabase (Free → $25/mo)

Property Value
Free tier 500 MB DB — pauses after 7 days inactivity
Pro plan $25/mo — always-on, no pausing
Managed Identity auth ❌ Connection string only
Extras Auth, object storage, edge functions bundled

More expensive than Neon for pure Postgres. Only worth it if bundled features (auth, storage) would replace other services. Not relevant here.


Option 4 — Railway (~$5–10/mo)

Property Value
Cost Usage-based, ~$5–10/mo for low traffic
Free credit $5/mo
Managed Identity auth ❌ Connection string only
Notes Good DX, less enterprise-grade, no Azure integration

Cost Summary

Option Monthly cost MI auth Notes
Azure B1ms (Burstable) ~$12/mo Blocked by northeurope capacity (transient)
Neon Launch $19/mo Cheapest if MI auth removed
Neon Free $0/mo Dev only — scale-to-zero, 191 compute-hours
Railway ~$5–10/mo Usage-based
Supabase Pro $25/mo Overkill
Azure D2s_v3 (current) ~$44/mo Status quo

Recommendation

Keep Azure Flexible Server, downgrade to B1ms when northeurope capacity recovers.

The existing Managed Identity auth stack (AAD-only, no secrets, ManagedIdentityCredential, AAD admin registration) is worth preserving:

  • No connection string secrets to rotate
  • Zero-trust posture consistent with the rest of the Azure setup
  • Full parity with production in CI/CD and Bicep modules

Any external provider (Neon, Supabase, Railway) requires removing the MI auth code and substituting a password — a security regression and non-trivial code change for ~$7/mo saving over the B1ms target price.

When to reconsider Neon

If per-PR preview databases become a priority (see plan-api-backed-experience-skills.md and pr-preview-envs backlog card), Neon's branching feature becomes a genuine advantage that justifies the MI auth trade-off. At that point, Track B below is the implementation path.


Track B — Neon Migration (if chosen later)

What changes

Layer Change
infra/modules/postgres.bicep Replace with neon_postgres.bicep — writes connection string to KV only
infra/main.bicep Add @secure() param neon_database_url; swap module; remove MI params
infra/modules/container_apps.bicep Remove USE_MANAGED_IDENTITY env var injection (~3 lines)
api/db/engine.py Remove IMDS token-fetch branch; always use connect_args={'ssl': 'require'}
deploy-infrastructure.yml Add --parameters neon_database_url="${{ secrets.NEON_DATABASE_URL }}"
GitHub Secrets Add NEON_DATABASE_URL per environment (dev, prod)
docker-compose.yml No change — local dev already uses password auth
alembic/env.py No change — already reads DATABASE_URL from env

New infra/modules/neon_postgres.bicep

bicep
// Neon Postgres module — writes connection string to Key Vault.
// Neon project/database are managed externally at console.neon.tech.
@secure()
param neon_database_url string
param key_vault_name string

resource key_vault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: key_vault_name
}

resource database_url_secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
  parent: key_vault
  name: 'database-url'
  properties: {
    value: neon_database_url
    contentType: 'text/plain'
  }
}

Execution order

  1. Create Neon account at console.neon.tech
  2. Create project portfolio, database portfolio, role portfolio_app
  3. Copy connection string: postgresql+asyncpg://<role>:<password>@<host>/portfolio?ssl=require
  4. Use -pooler hostname suffix for built-in PgBouncer connection pooling
  5. Add NEON_DATABASE_URL as GitHub Secret on dev and prod environments
  6. Create infra/modules/neon_postgres.bicep
  7. Update infra/main.bicep — add param, swap module, remove MI params
  8. Update infra/modules/container_apps.bicep — remove USE_MANAGED_IDENTITY injection
  9. Update deploy-infrastructure.yml — add NEON_DATABASE_URL param passthrough
  10. Simplify api/db/engine.py — remove MI branch, always use SSL auth
  11. Run migrations against Neon before cutover:
DATABASE_URL=<neon-url> alembic upgrade head
  1. Deploy infra → containers redeploy → verify /api/health and DB-backed endpoints
  2. If prod data exists: dump and restore

    pg_dump -h <fqdn> -U <temp-user> -d portfolio -F c -f portfolio.dump
    pg_restore -d "<neon-url>" -F c portfolio.dump
    
  3. Delete Azure Flexible Server; set deploy_postgres = false permanently

Trade-offs

Concern Detail
No managed identity Password stored in Key Vault. Rotation needs to be defined.
Cross-cloud latency Neon runs on AWS Frankfurt. Adds ~5–20ms vs same-region Azure Postgres.
Double cold start ACA + Neon both scale to zero on dev. ~3–4s combined cold start. Use -pooler hostname.
Cost at scale Neon Launch $19/mo vs Azure B1ms ~$12/mo. Neon more expensive at higher utilisation.
Benefit Detail
DB branching Instant DB branches per PR — enables per-PR preview databases
Simpler auth code Removes IMDS token-fetch complexity from engine.py
CI-accessible Publicly reachable over SSL — migrations run from GitHub Actions without network tricks
Free tier for dev Neon free tier sufficient for dev workloads
Connection pooler Built-in PgBouncer via -pooler hostname suffix