Skip to content
PostgreSQL permission denied for sequence: Fix

Click to use (opens in a new tab)

PostgreSQL permission denied for sequence: Fix

November 4, 2026 by Chat2DBChat2DB Team

You gave your application role INSERT on the table, you checked it twice, and the insert still fails with an error about something you never mentioned:

ERROR:  permission denied for sequence orders_id_seq

The table permission is fine. The problem is that a serial column gets its value from a separate object, a sequence, and sequences have their own privileges. Granting access to a table does nothing for the sequence behind it. SQLSTATE is 42501, the general insufficient_privilege code.

The quick fix is one line. The useful part of this guide is the rest: which privilege you need, why the error returns next month for a new table, and how identity columns make the whole problem disappear. All the output below comes from PostgreSQL 18.6.

The Quick Fix

Grant USAGE on the sequence to the role that inserts:

GRANT USAGE ON SEQUENCE orders_id_seq TO app;

If you are not sure of the name, ask PostgreSQL:

SELECT pg_get_serial_sequence('orders', 'id');
 pg_get_serial_sequence
------------------------
 public.orders_id_seq

To fix every existing sequence in a schema in one statement:

GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app;

That solves today's error. If you run the application from a schema other than public, change the schema name, and repeat for each schema you use.

Reproducing the Error

Here are two tables that look almost the same. One uses serial, the other an identity column, and the role gets SELECT and INSERT on both:

CREATE ROLE app LOGIN PASSWORD 'x';
 
CREATE TABLE o  (id serial PRIMARY KEY, v text);
CREATE TABLE o2 (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, v text);
 
GRANT SELECT, INSERT ON o, o2 TO app;
 
SET ROLE app;
 
INSERT INTO o (v) VALUES ('a');
-- ERROR:  permission denied for sequence o_id_seq
 
INSERT INTO o2 (v) VALUES ('a');
-- INSERT 0 1

The serial table fails and the identity table works. This difference is the key to the permanent fix, so we will come back to it.

Why This Happens

serial is not a real type. It is shorthand that creates a sequence and sets the column default to call it:

SELECT pg_get_expr(adbin, adrelid)
FROM pg_attrdef
WHERE adrelid = 'o'::regclass;
 nextval('o_id_seq'::regclass)

So each insert runs nextval() as the role doing the insert, and PostgreSQL checks that role's privileges on the sequence. Your INSERT privilege covers the table only.

An identity column also uses a sequence underneath, but PostgreSQL treats that sequence as an internal part of the column. The dependency type in the catalog shows the difference:

SELECT c.relname, d.deptype
FROM pg_class c
JOIN pg_depend d ON d.objid = c.oid
WHERE c.relkind = 'S'
  AND d.deptype IN ('a', 'i');
  relname  | deptype
-----------+---------
 o_id_seq  | a
 o2_id_seq | i

Type a means automatic, an ordinary owned sequence that needs its own grants. Type i means internal, and access follows the table.

Which Privilege Do You Need?

Sequences have three privileges, and they do not work the way their names suggest. We tested each one on its own:

OperationUSAGESELECTUPDATE
nextval(), including inserts through a defaultYesNoYes
currval()YesYesNo
SELECT last_value FROM seqNoYesNo
setval()NoNoYes

Some of this is worth seeing in practice. With USAGE only, the insert and currval() work, but reading the sequence directly and changing it do not:

GRANT USAGE ON SEQUENCE o_id_seq TO app;
 
SET ROLE app;
INSERT INTO o (v) VALUES ('b') RETURNING id;   -- 1
SELECT currval('o_id_seq');                    -- 1
SELECT last_value FROM o_id_seq;
-- ERROR:  permission denied for sequence o_id_seq
SELECT setval('o_id_seq', 5);
-- ERROR:  permission denied for sequence o_id_seq

So USAGE is the right grant for an application. RETURNING id and currval() both work, and the role cannot reset the sequence. That last point matters more than it looks: a role that can call setval() can move the counter backwards, and every later insert then collides with existing rows until someone notices. Add SELECT only if something reads last_value, as some ORMs and monitoring tools do. Keep UPDATE for migration or data-loading roles, because setval() can cause duplicate key errors for everyone if it is misused.

Note that UPDATE on its own also allows nextval(), which is surprising, but a role with only SELECT cannot:

-- role with UPDATE only
SELECT nextval('ord_id_seq');   -- 1
 
-- role with SELECT only
SELECT nextval('ord_id_seq');
-- ERROR:  permission denied for sequence ord_id_seq

Why the Error Comes Back

The grant above covers sequences that exist now. Create a new table next month and you are back where you started:

GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app;
 
CREATE TABLE o3 (id serial PRIMARY KEY);
GRANT INSERT ON o3 TO app;
 
SET ROLE app;
INSERT INTO o3 DEFAULT VALUES;
-- ERROR:  permission denied for sequence o3_id_seq

ALL SEQUENCES means all sequences at this moment. To cover future ones, set default privileges:

ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO app;
 
CREATE TABLE o4 (id serial PRIMARY KEY);
GRANT INSERT ON o4 TO app;
 
SET ROLE app;
INSERT INTO o4 DEFAULT VALUES;
-- INSERT 0 1

There is a trap here that causes a lot of confusion. Default privileges apply only to objects created by the role that ran the ALTER DEFAULT PRIVILEGES statement. If you run it as postgres and your migrations run as deploy, new sequences get nothing, and the error appears to come back for no reason. Name the creating role explicitly:

ALTER DEFAULT PRIVILEGES FOR ROLE deploy IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO app;

Set the same thing for tables at the same time, so a new table is usable straight away. Our schema grants guide covers the full pattern for read and write roles, and the free Postgres Grant Generator (opens in a new tab) writes these statements for you.

Audit Every Table at Once

If you inherited a database, find the gaps before your users do. This query lists each serial sequence where a role can insert into the table but cannot use the sequence:

SELECT n.nspname AS schema, t.relname AS table_name,
       a.attname AS column_name, s.relname AS sequence_name
FROM pg_class s
JOIN pg_namespace n ON n.oid = s.relnamespace
JOIN pg_depend d ON d.objid = s.oid
  AND d.classid = 'pg_class'::regclass AND d.deptype = 'a'
JOIN pg_class t ON t.oid = d.refobjid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid
WHERE s.relkind = 'S'
  AND has_table_privilege('app', t.oid, 'INSERT')
  AND CASE WHEN s.relkind = 'S'
           THEN NOT has_sequence_privilege('app', s.oid, 'USAGE')
           ELSE false END;
 schema | table_name | column_name | sequence_name
--------+------------+-------------+---------------
 public | a          | id          | a_id_seq

In our test, this found the one table missing a grant, and it correctly skipped the table with a grant and the identity column. The CASE is not decoration. Without it, PostgreSQL can evaluate has_sequence_privilege() on a row that is not a sequence and stop with an error such as "pg_toast_204235" is not a sequence.

To check a single sequence, use the function directly or look at it in psql:

SELECT has_sequence_privilege('app', 'o_id_seq', 'usage');
\dp o_id_seq

In \dp output, r means SELECT, w means UPDATE and U means USAGE. A line like app=rwU/postgres means the role has all three.

The Permanent Fix: Use Identity Columns

For new tables, the best way to avoid this error is to stop using serial:

CREATE TABLE invoices (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  total numeric(12, 2) NOT NULL
);

Our test showed the benefit at the start: the identity table accepted the insert with no sequence grant at all. A role with INSERT on the table has what it needs, so there is one less thing to forget. Identity columns are also standard SQL, and our serial vs identity guide covers the other advantages.

You can convert an existing serial column without rewriting the table. Remove the default and the old sequence, then add identity:

BEGIN;
ALTER TABLE a1 ALTER COLUMN id DROP DEFAULT;
DROP SEQUENCE a1_id_seq;
ALTER TABLE a1 ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY;
COMMIT;
SET ROLE app2;
INSERT INTO a1 DEFAULT VALUES;
-- INSERT 0 1

Before you run this on a table with data, note the current sequence value, because the new identity sequence starts at 1 and must be moved past your highest id. The Serial to Identity Converter (opens in a new tab) generates the full script, including that step.

Related Situations

The error appears after a restore. Grants come from the dump, so check that it was not taken with --no-privileges or restored with --no-acl. See the pg_restore guide for those options.

The error names a different table. A trigger or audit function inserts into another table, using its own sequence. Fix the grant for that sequence, or review whether the function should be SECURITY DEFINER.

A role can insert through one path but not another. Check membership with \du. Privileges of a parent role can apply only after SET ROLE if the role was created with NOINHERIT.

An ORM reads the sequence directly. Some frameworks and admin tools run SELECT last_value or similar queries to inspect a table, and they fail with this same error even though ordinary inserts work. The privilege table above explains why: USAGE does not allow it, and adding SELECT does.

The role gets a table error next. Fixing the sequence can reveal the next missing grant. In our test, INSERT on o3 with RETURNING id went on to fail with permission denied for table o3, because RETURNING needs SELECT on the column as well. Grant it if your ORM uses RETURNING. The permission denied for table guide covers that case.

Checklist

  1. Find the sequence with pg_get_serial_sequence().
  2. Grant USAGE to the inserting role, and SELECT only if needed.
  3. Run the same grant for all existing sequences in the schema.
  4. Add ALTER DEFAULT PRIVILEGES FOR ROLE for the role that creates tables.
  5. Audit with has_sequence_privilege().
  6. Use identity columns for new tables.

Manage Privileges Without the Guesswork

Permission errors are easier to fix when you can see what each role holds. In Chat2DB (opens in a new tab) you can inspect tables and sequences side by side, run the audit query above across schemas, and ask the AI assistant to write the exact GRANT statement for your role. It also works in the browser at app.chat2db.ai (opens in a new tab).