All guidesMore providers: databases, auth, hosting, email and payments
On this page

Rails and Supabase error

SupabaseRails

PG::DuplicatePstatement: prepared statement "a1" already exists

By Ohad Krispin, founder of VibeRaven · LinkedIn

"PG::DuplicatePstatement: prepared statement "a1" already exists" means Rails sent a named prepared statement through Supabase's shared pooler in transaction mode, which is port 6543. Rails names its statements a1, a2 and so on and prepares them per database connection. Supabase says the shared pooler does not support prepared statements in transaction mode. Fix it one of two ways: set prepared_statements: false on the production database config, or add ?prepared_statements=false to DATABASE_URL; or move the string to the shared pooler's session mode on port 5432, which supports them. Then deploy again.

Why does it start after I switch DATABASE_URL to port 6543?

Supabase lists three ways in. The direct connection on port 5432 is IPv6 unless the project has the IPv4 add-on. The shared pooler is IPv4-only on every plan, in session mode on port 5432 and in transaction mode on port 6543. Supabase recommends transaction mode for serverless and edge functions, which open many short-lived connections, and says the shared pooler does not support prepared statements in transaction mode. Its troubleshooting page adds that direct connections and session mode do. A reply on a Fly.io forum thread with the same error on port 6543 puts it this way: prepared statements are created per connection, not per transaction, so the app has to turn them off.

Active Record prepares statements by default. In the Rails source the PostgreSQL adapter names them with the prefix a and a counter, which is where "a1" comes from, and the default for the prepared_statements setting is true. If you moved to the pooler because the direct host failed, see the Render page or the Railway page.

# direct, IPv6 unless the IPv4 add-on is on
postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres
# shared pooler, session mode: prepared statements work
postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:5432/postgres
# shared pooler, transaction mode: no prepared statements
postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:6543/postgres

How do I turn prepared statements off in Rails?

The adapter reads prepared_statements from the database config and treats the string "false" as false, so both forms below work. Supabase's page on disabling prepared statements lists Prisma, Drizzle, Node Postgres, Psycopg, asyncpg and JDBC but not Rails, so the Rails setting comes from the Active Record source. The Rails guide also says Active Record uses advisory locks by default, that you might need to disable them behind an external pooler such as PgBouncer, and gives advisory_locks: false for it. The migrator takes that lock in the Rails source.

# config/database.yml
production:
  adapter: postgresql
  url: <%= ENV["DATABASE_URL"] %>
  prepared_statements: false
  advisory_locks: false

# or in the URL itself
postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:6543/postgres?prepared_statements=false

Which mode should a Rails app on Render, Railway, Fly.io or Heroku use?

These hosts run a long-lived process, not a function per request. Supabase says to use session mode on the shared pooler when you connect from an IPv4-only network, and that it supports prepared statements, cursors and session-level state. That keeps the Rails defaults and needs no code change. Choose transaction mode only when many short-lived clients would exhaust connections, and then turn prepared statements off. Supabase lists migrations as a use for the direct connection, so run bin/rails db:migrate with a direct or session string, not the transaction one.

Supabase's page also says the dedicated pooler supports prepared statements on PgBouncer 1.21 and later. That does not apply to the shared pooler in transaction mode.

Checklist

How to fix it, then check the fix

  1. 01On the host, open the DATABASE_URL variable. A pooler host and port 6543 mean transaction mode.
  2. 02Pick one. Either switch the port to 5432 on the same pooler host (session mode), or keep 6543 and set prepared_statements: false.
  3. 03
    Find where the app reads the connection:
    grep -rnE "DATABASE_URL|prepared_statements|advisory_locks|statement_limit" config
  4. 04If you keep 6543, set prepared_statements: false and advisory_locks: false under production in config/database.yml, or add ?prepared_statements=false to the URL.
  5. 05Save the variable and deploy again. Railway asks you to click Deploy after you edit variables; Heroku restarts the app on a config var change.
  6. 06
    Check the setting from a production console, then load a few pages twice:
    bin/rails runner 'puts ActiveRecord::Base.connection.prepared_statements'
  7. 07The command prints false on port 6543, and the log has no PG::DuplicatePstatement.

Related

Sources

Official docs behind this checklist

FAQ

FAQ for builders before launch

Why did it work on my machine?

The Rails guide says the generated development.rb turns on query_log_tags_enabled, and that this disables prepared statements automatically. You may also be connected to a direct or local database. Production has neither.

Does disabling prepared statements break anything?

Rails then sends each query without preparing it first. The Rails guide notes that every prepared statement costs database memory, which is why it suggests disabling them when memory is tight.

Is the error a Supabase outage?

No. The pooler works as documented: in transaction mode it does not support prepared statements. The change is in the Rails config or in the port you connect to.