Nobody looks up max_function_args on purpose. You meet it when PostgreSQL interrupts something unrelated with cannot pass more than 100 arguments to a function, and you go looking for the knob. There isn’t one.
It’s a read-only preset, a sibling of block_size and integer_datetimes. The context is internal; SET, ALTER SYSTEM, and a line in postgresql.conf all produce parameter "max_function_args" cannot be changed, and pg_settings reports a min_val and a max_val of 100 each, a range with one member. What it reports is FUNC_MAX_ARGS from src/include/pg_config_manual.h, which has been 100 since PostgreSQL 8.1.
Before that it wasn’t a number. Through 8.0, FUNC_MAX_ARGS was #defined as INDEX_MAX_KEYS, with a comment insisting the two “must be the same value.” The reason was catalog plumbing: oidvector was a fixed-width C array of INDEX_MAX_KEYS OIDs, and it was the type of both pg_index.indclass and pg_proc.proargtypes, so one width had to serve both. The argument limit was therefore 16 in 7.2 and 32 from 7.3, and the GUC that reports it appeared in 8.0 announcing 32. In March 2005 Tom Lane made oidvector and int2vector varlenas, which removed the reason the constants had to match, and a few hours later decoupled them and set FUNC_MAX_ARGS to 100. Why 100 and not 500? His estimate on pgsql-hackers was that the code was full of MemSet calls clearing FUNC_MAX_ARGS worth of memory, and that 100 or so could be had without pain. Index keys stayed at 32, and max_index_keys is the other half of what used to be one number.
The MemSet worry was real, and it outlived the change by fourteen years. Until PostgreSQL 12, FunctionCallInfoData, the struct every V1 function receives its arguments in, carried two fixed arrays of FUNC_MAX_ARGS entries, one of Datum and one of bool. A two-argument call to int4pl was handed 936 bytes of struct on x86-64. Andres Freund’s rewrite in 12 made the struct variable-length (that call is now 64 bytes), and what’s left of the constant is mostly stack arrays: in the parser, and in a few places that don’t know their argument count at compile time. Since 2005 it hasn’t been part of the on-disk format at all, which is why the header has said since 8.1 that raising it needs no initdb.
Two places enforce it, and they produce different sentences. ProcedureCreate counts a function’s input parameters (OUT parameters explicitly don’t count; a function with 100 IN parameters and three OUT ones creates fine) and refuses the 101st with functions cannot have more than 100 arguments. You get that wording for CREATE PROCEDURE too (the source does contain procedures cannot have more than 100 arguments, but only on the path you reach by trying to DROP one). Aggregates get 99, because the transition function’s first argument is the state.
The other place is the parser, and it’s the one people hit. ParseFuncOrColumn checks the raw argument list before it does anything else, before it has resolved which function you mean, because the rest of the parser keeps Oid arrays of FUNC_MAX_ARGS entries on the stack and would rather not overrun them. So cannot pass more than 100 arguments to a function (SQLSTATE 54023, too_many_arguments) fires on the literal count of things between the parentheses, and it fires for variadic functions, whose entire purpose is to accept an indefinite number of them. concat() with 101 strings fails. format() with a hundred placeholders fails, because the format string is the hundred-and-first argument. jsonb_build_object(), which takes key/value pairs, fails at the fifty-first pair. That last one is how most people meet it: a table grows its fifty-first column, the ORM that renders a related row as jsonb_build_object('id', t.id, 'name', t.name, ...) emits 102 arguments, and a query that has worked since the project started stops. The 2020 bug report asking for 500 on exactly these grounds got the answer it was always going to get.
What the limit does not apply to is anything that looks like a function call but isn’t one to the parser. COALESCE, GREATEST, LEAST, ROW(...), ARRAY[...], and x IN (...) are expression nodes of their own kind, and I fed each of them 500 arguments on 18.6 without complaint.
The fix is on your side of the socket, and it’s the same fix every time: one argument that holds many things. VARIADIC ARRAY[...] is a single argument as far as the parser is concerned, and it works for concat(VARIADIC ARRAY[...]) and for jsonb_build_object(VARIADIC ARRAY[...]), with the caveat that an array has one element type, so keys and values all have to be text and the values come out as JSON strings. For the wide-table JSON case, to_jsonb(t) builds the whole row as one argument with the types intact, and the - operator removes the columns you didn’t want. For the 150-parameter stored procedure arriving from an Oracle or SQL Server migration, pass a composite type or a jsonb document. Tom Lane’s advice in 2006 to someone with a 65-argument PL/pgSQL function was to rethink the API, and it has not gone out of date.
You can, in principle, raise it. It’s a one-line edit; the header allows anything from 8 (GIN support functions take eight arguments) to roughly 600 (the pg_proc index tuple has to hold the argument-type vector), and no initdb is needed. What is needed is a rebuild of every C extension on the machine, because since 8.2 the PG_MODULE_MAGIC block has recorded FUNC_MAX_ARGS, and dfmgr.c compares it at load time. Here is 18.6, built from the official tarball with the one line changed to 200, being offered the pg_stat_statements shipped in the PGDG 18.6 package:
1 postgres=# SHOW max_function_args;
2 max_function_args
3 -------------------
4 200
5 (1 row)
6
7 postgres=# LOAD '/usr/lib/postgresql/18/lib/pg_stat_statements.so';
8 ERROR: incompatible library "/usr/lib/postgresql/18/lib/pg_stat_statements.so": magic block mismatch
9 DETAIL: Server has FUNC_MAX_ARGS = 200, library has 100.
Same source, same minor release, different header, no deal. PostGIS, pgvector, and every contrib module are in the same position, and on a managed service the question never comes up because you don’t build the server.
The number is 100, it has been 100 for two decades, and you will not be running a server where it’s anything else. A query that wants a hundred and one arguments wants a list. Give it one, and the limit goes back to being something nobody looks up.