PostgreSQL GUC Parameters — config_generic, PGC Contexts, SET/SHOW, Check/Assign Hooks, and SIGHUP Reload
Contents:
- Theoretical Background
- Common DBMS Design
- PostgreSQL’s Approach
- Source Walkthrough
- Source verification (as of 2026-06-05)
- Beyond PostgreSQL — Comparative Designs & Research Frontiers
- Sources
Theoretical Background
Section titled “Theoretical Background”Every database engine ships with hundreds of behavioral knobs — buffer-pool
size, planner cost factors, WAL flush policy, lock timeouts, logging
verbosity. Collectively these are the system’s configuration surface, and
the design of that surface is a first-order engineering concern, not an
afterthought. Architecture of a Database System (Hellerstein, Stonebraker &
Hamilton 2007, captured in dbms-papers/fntdb07-architecture.md) frames the
DBMS as a set of cooperating components — process manager, query processor,
storage manager, shared utilities — and observes that nearly every one of
those components has tunables that a DBA, an automated tuner, or the server’s
own startup logic must set. The hard part is not storing a number; it is
imposing discipline on a sprawling, heterogeneous set of values so the
system stays coherent.
Four cross-cutting concerns shape any configuration subsystem:
-
Typing and validation. A knob is a boolean, an integer with a range, a real, a string, or an enumeration over named alternatives. Setting it must reject ill-typed or out-of-range input before the value reaches the code that depends on it. A value like
work_mem = 4MBalso carries a unit that must be normalized to a canonical base unit. -
Authority and timing — who may change it, and when. Some parameters are immutable after process start (the size of a shared-memory segment cannot change while backends are mapped to it). Some require operator privilege (turning off
fsyncis not something an ordinary user should do). Some are per-session and freely settable by anyone (work_memwithin a query). A configuration system needs a lattice of “contexts” that says, for each parameter, the earliest binding time and the minimum privilege. -
Layered sources with precedence. The effective value of a parameter is the result of overlaying several sources: a compiled-in default, an environment variable, the command line, the config file, a per-database or per-role default, and finally an interactive
SET. The system must track where the current value came from so a lower-priority source (re-reading the file) does not clobber a higher-priority one (the command line). -
Scoping and rollback. An interactive change made inside a transaction that later aborts must revert. A
SET LOCALmust revert at transaction end. A function with aSETclause must revert when the function returns. This demands a stack of saved values keyed to transaction nesting depth.
These are the same disciplines a programming-language runtime imposes on its dynamically-scoped variables, which is exactly the lineage of PostgreSQL’s name for the subsystem: the GUC, the “Grand Unified Configuration.” The goal of this document is to show how PostgreSQL meets all four concerns with a single table-driven mechanism rather than four bespoke ones.
A fifth concern is subtler but equally important: the read path must be
cheap. Configuration values are read constantly — the planner consults
enable_seqscan, random_page_cost, and a dozen others to cost a single
plan; the executor checks work_mem per sort node; every ereport consults
log_min_messages. If reading a parameter meant a hash lookup or a catalog
probe, configuration would tax the hottest code in the system. The discipline,
then, is to make writes go through a rich validated funnel while reads stay
a bare load of an ordinary C global. PostgreSQL achieves this by storing the
live value in a plain variable (int work_mem;) and keeping the GUC record’s
variable field pointing at it; the GUC machinery writes through that pointer,
and the consuming code reads the global directly, never touching the GUC layer
on the read path. This split — heavyweight write, zero-cost read — is the
quiet architectural decision that makes a table of a thousand knobs affordable.
Common DBMS Design
Section titled “Common DBMS Design”Across engines, three recurring shapes appear:
-
A flat key-value catalog table. The simplest design stores parameters as rows
(name, value)in a system table, validated by application logic on write. This is trivial to introspect via SQL but pushes type-safety and binding-time rules into scattered call sites, and it offers no natural place for the per-variable hook logic that complex knobs need. -
A statically-typed descriptor array. The richer design — PostgreSQL’s — declares each parameter as a struct in a compiled array: name, type, range, default, the address of the C variable that holds the live value, and optional callback functions for validation and side effects. The descriptor is the single source of truth; SQL introspection (
pg_settings) is a view computed from the array, not the storage. This keeps the hot read path a bare C-variable dereference (no catalog lookup to readwork_memon every plan node) while still exposing everything to SQL. -
File reload vs. restart. Every server distinguishes parameters that can be re-read live from a config file from those frozen at process start. PostgreSQL, MySQL, and Oracle all expose a “reload” operation (PostgreSQL’s
SIGHUP/pg_reload_conf(), MySQL’sSET GLOBAL+ config, Oracle’sALTER SYSTEM ... SCOPE=BOTH) and a separate class of restart-only parameters.
A subtle pitfall every multi-source design must avoid is clobbering a
higher-priority value with a lower-priority one. When the config file is
re-read on reload, the file’s values must not override a command-line switch
or an interactive SET that outranks them. Engines solve this by tagging each
live value with its provenance and refusing a downgrade. PostgreSQL’s
GucSource ordering does exactly this: a new value takes effect only if its
source ranks at least as high as the one that set the current value, which lets
the server re-process sources in any convenient order — file, then auto.conf,
then per-database defaults — without the order affecting the result.
Enumerated parameters deserve special mention. A knob like
client_min_messages or bytea_output ranges over a fixed set of named
alternatives, and a good design validates the name against that set and stores
the resolved integer, so the consuming code switches on an int rather than
re-parsing a string. PostgreSQL models this with config_enum plus a
config_enum_entry[] options array; an entry may be hidden (accepted on
input but omitted from the list of legal values shown to users), which is how
deprecated spellings are kept working without advertising them.
The descriptor-array approach buys two things textbooks rarely emphasize. First, the validation and side-effect logic lives next to the declaration, as function pointers in the descriptor — so the knob and its semantics travel together. Second, binding time becomes a first-class field rather than an implicit convention, so the engine can mechanically reject “you cannot change that now” without each subsystem re-implementing the rule. PostgreSQL pushes this further than most: the same funnel function applies the context rule, the type/range check, the per-variable hook, the transactional stack, and the client-notification bookkeeping, so a new parameter inherits all of it for free.
PostgreSQL’s Approach
Section titled “PostgreSQL’s Approach”One header, five typed records
Section titled “One header, five typed records”Every GUC is a statically-initialized struct whose first member is a
shared config_generic header. C’s guarantee that a struct’s first member
shares the struct’s address lets generic code cast any typed record to
config_generic * and back, exactly the way PostgreSQL’s Node system works.
// struct config_generic — src/include/utils/guc_tables.hstruct config_generic{ /* constant fields, must be set correctly in initial value: */ const char *name; /* name of variable - MUST BE FIRST */ GucContext context; /* context required to set the variable */ enum config_group group; /* to help organize variables by function */ const char *short_desc; /* short desc. of this variable's purpose */ const char *long_desc; /* long desc. of this variable's purpose */ int flags; /* flag bits, see guc.h */ /* variable fields, initialized at runtime: */ enum config_type vartype; /* type of variable (set only at startup) */ int status; /* status bits, see below */ GucSource source; /* source of the current actual value */ GucSource reset_source; /* source of the reset_value */ GucContext scontext; /* context that set the current value */ GucContext reset_scontext; /* context that set the reset value */ Oid srole; /* role that set the current value */ Oid reset_srole; /* role that set the reset value */ GucStack *stack; /* stacked prior values */ void *extra; /* "extra" pointer for current actual value */ /* ... dlist/slist links for nondef / stack / report lists ... */ char *sourcefile; /* file current setting is from */ int sourceline; /* line in source file */};The five concrete record types embed that header and add the type-specific fields. The boolean record is representative:
// struct config_bool — src/include/utils/guc_tables.hstruct config_bool{ struct config_generic gen; /* constant fields, must be set correctly in initial value: */ bool *variable; /* address of the live C variable */ bool boot_val; /* compiled-in default */ GucBoolCheckHook check_hook; GucBoolAssignHook assign_hook; GucShowHook show_hook; /* variable fields, initialized at runtime: */ bool reset_val; /* value RESET returns to */ void *reset_extra;};config_int and config_real additionally carry min/max; config_enum
carries an options array of config_enum_entry name→value pairs (and its
variable is an int * that receives the resolved enum value); and
config_string holds a char **variable plus a const char *boot_val. An
enum table entry pairs the variable with its options array and a default
member:
// ConfigureNamesEnum[] — src/backend/utils/misc/guc_tables.c{ {"bytea_output", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets the output format for bytea."), NULL }, &bytea_output, /* int *variable */ BYTEA_OUTPUT_HEX, /* boot_val (an enum member) */ bytea_output_options, /* config_enum_entry[] */ NULL, NULL, NULL},The
variable field is the crux: it is the address of an ordinary C global (e.g.
&work_mem, &enable_seqscan) that the rest of the backend reads directly
with zero indirection through the GUC machinery. The GUC layer’s job is to
assign that variable correctly; reading it is a bare load.
The boot_val is the compiled-in default that every variable holds before any
source is consulted; reset_val is the value RESET returns to, normally
established at startup from the highest-priority non-interactive source (file,
command line, or environment) so that RESET work_mem inside a session drops
back to the configured baseline rather than the hard-coded default. The
reset_source / reset_scontext / reset_srole fields mirror this for the
reset value, and the srole pair records which role set a value — needed
because a SET made by a superuser and one made by a delegated non-superuser
must be distinguished when the security context unwinds.
The records live in five hand-written arrays in guc_tables.c, each
NULL-name-terminated. A typical entry is a nested brace-initializer — the
inner braces fill the config_generic header, the outer ones the typed tail:
// ConfigureNamesBool[] — src/backend/utils/misc/guc_tables.c{ {"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD, gettext_noop("Enables the planner's use of sequential-scan plans."), NULL, GUC_EXPLAIN }, &enable_seqscan, /* variable */ true, /* boot_val */ NULL, NULL, NULL /* check_hook, assign_hook, show_hook */},// ConfigureNamesInt[] — src/backend/utils/misc/guc_tables.c{ {"min_dynamic_shared_memory", PGC_POSTMASTER, RESOURCES_MEM, gettext_noop("Amount of dynamic shared memory reserved at startup."), NULL, GUC_UNIT_MB }, &min_dynamic_shared_memory, 0, 0, (int) Min((size_t) INT_MAX, SIZE_MAX / (1024 * 1024)), NULL, NULL, NULL},Note how the PGC_USERSET vs PGC_POSTMASTER context and the
GUC_EXPLAIN / GUC_UNIT_MB flags are declarative — they sit in the table,
and the generic machinery interprets them. The config_group field
(QUERY_TUNING_METHOD, RESOURCES_MEM, …) is a coarse taxonomy from
enum config_group used only to organize pg_settings output and the sample
config file.
The context ladder — who, and when
Section titled “The context ladder — who, and when”GucContext is the privilege-and-timing lattice. Read top to bottom, each
level is strictly more permissive about binding time:
// GucContext — src/include/utils/guc.htypedef enum{ PGC_INTERNAL, /* cannot be set by the user at all */ PGC_POSTMASTER, /* only at server start (postgresql.conf / command line) */ PGC_SIGHUP, /* server-wide, changeable on config reload */ PGC_SU_BACKEND, /* at connection start; superuser/granted at startup pkt */ PGC_BACKEND, /* at connection start; anyone via startup packet */ PGC_SUSET, /* superuser (or granted) any time, incl. SET */ PGC_USERSET, /* anyone, any time, via SET */} GucContext;This single enum encodes both binding time (when the value may first be
fixed) and authority (what privilege a setter needs). shared_buffers is
PGC_POSTMASTER because the shared-memory layout is computed once at startup;
fsync is PGC_SIGHUP so it can be flipped on reload but only by editing a
file the DBA controls; work_mem is PGC_USERSET so any client can raise it
for its own session. The orthogonal GucSource enum records where the
current value came from so that re-processing a lower-priority source never
overrides a higher one:
// GucSource — src/include/utils/guc.h (abridged)typedef enum{ PGC_S_DEFAULT, /* hard-wired default ("boot_val") */ PGC_S_DYNAMIC_DEFAULT, /* default computed during initialization */ PGC_S_ENV_VAR, /* postmaster environment variable */ PGC_S_FILE, /* postgresql.conf */ PGC_S_ARGV, /* postmaster command line */ PGC_S_GLOBAL, PGC_S_DATABASE, PGC_S_USER, PGC_S_DATABASE_USER, PGC_S_CLIENT, /* from client connection request */ PGC_S_OVERRIDE, /* special case to forcibly set default */ PGC_S_INTERACTIVE, /* dividing line for error reporting */ PGC_S_TEST, /* test per-database or per-user setting */ PGC_S_SESSION, /* SET command */} GucSource;The two enums are independent axes: context is a property of the variable
(fixed in the table), while source is a property of the current value and
changes over the variable’s life. The set-path checks the requesting context
against the variable’s declared context, and stamps the source onto the
value it commits.
The ladder is enforced by a single switch (record->context) near the top of
the funnel. The most interesting rungs are the two that are not simple
accept/reject. PGC_POSTMASTER accepts a config-file re-read at PGC_SIGHUP
time only to compare — it sets a flag and refuses the change later if the
canonicalized value actually differs, which is how the server tells a DBA “you
edited a restart-only parameter.” PGC_SU_BACKEND and PGC_SUSET perform a
privilege check via pg_parameter_aclcheck, so a parameter can be delegated to
a non-superuser with GRANT SET ON PARAMETER:
// set_config_with_handle (context switch, abridged) — src/backend/utils/misc/guc.cswitch (record->context){ case PGC_INTERNAL: if (context != PGC_INTERNAL) ereport(elevel, (errmsg("parameter \"%s\" cannot be changed", ...))); break; case PGC_POSTMASTER: if (context == PGC_SIGHUP) prohibitValueChange = true; /* re-read to compare, not apply */ else if (context != PGC_POSTMASTER) ereport(elevel, (errmsg("... cannot be changed without restarting ..."))); break; case PGC_SUSET: if (context == PGC_USERSET || context == PGC_BACKEND) { AclResult aclresult = pg_parameter_aclcheck(record->name, srole, ACL_SET); if (aclresult != ACLCHECK_OK) ereport(elevel, (errmsg("permission denied to set parameter \"%s\"", ...))); } break; case PGC_USERSET: /* always okay */ break;}The prohibitValueChange flag is checked after the value is parsed and
canonicalized, because variant input formats (e.g. 1GB vs 1024MB) must be
normalized before the “did it actually change?” comparison is meaningful.
The write funnel and the lookup hash
Section titled “The write funnel and the lookup hash”At startup, build_guc_variables() walks the five arrays, stamps each
record’s vartype, and inserts every record into one case-insensitive
dynahash keyed on the name pointer. All later lookups go through
find_option(), which also lazily creates placeholder records for custom
dotted names (e.g. an extension’s mymodule.setting) so a parameter can be
referenced before the module that owns it is loaded.
Every write — SQL SET, config-file apply, ALTER SYSTEM, internal override
— funnels through set_config_with_handle() (the worker behind
set_config_option). That funnel applies the context ladder, validates and
canonicalizes the value, manages the transactional stack, commits the new
value, fires the assign hook, and flags client reporting — all in one place.
Lookups never scan the arrays. At startup the five tables are folded into one
dynahash; find_option() does the resolution and, crucially, can mint a new
record on demand for a custom dotted name an extension has not yet registered:
// add_guc_variable — src/backend/utils/misc/guc.chentry = (GUCHashEntry *) hash_search(guc_hashtab, &var->name, HASH_ENTER_NULL, &found);if (unlikely(hentry == NULL)) ereport(elevel, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of memory")));Assert(!found);hentry->gucvar = var;return true;A placeholder is created only if the name passes
valid_custom_variable_name() — “two or more identifiers separated by dots,”
the same lexical rule as scan.l — so mymodule.threshold is accepted as a
deferred custom GUC while a bare unknown name is rejected as an unrecognized
parameter. When the owning module later calls DefineCustomIntVariable, the
placeholder is promoted in place to a fully typed record, preserving any value
already assigned to it.
flowchart TD
A["SET work_mem='64MB'<br/>(ExecSetVariableStmt)"] --> B["set_config_option<br/>context=PGC_USERSET/SUSET<br/>source=PGC_S_SESSION"]
C["postgresql.conf line<br/>(ProcessConfigFile)"] --> D["set_config_option<br/>context=PGC_SIGHUP<br/>source=PGC_S_FILE"]
E["ALTER SYSTEM SET<br/>(writes auto.conf)"] --> C
B --> F["set_config_with_handle"]
D --> F
F --> G["find_option<br/>name -> config_generic"]
G --> H{"context ladder<br/>check: may this<br/>context set this var?"}
H -- "no" --> I["ereport: cannot be<br/>changed / needs restart /<br/>permission denied"]
H -- "yes" --> J["parse_and_validate_value<br/>type + range + check_hook"]
J -- "invalid" --> I
J -- "ok, newval+extra" --> K["push_old_value<br/>(stack for SET LOCAL / rollback)"]
K --> L["*variable = newval<br/>set source/scontext/srole"]
L --> M["assign_hook(newval, extra)<br/>side effect"]
M --> N{"GUC_REPORT?"}
N -- "yes" --> O["status |= GUC_NEEDS_REPORT<br/>-> ParameterStatus msg"]
N -- "no" --> P["done"]
Validation, hooks, and the extra pointer
Section titled “Validation, hooks, and the extra pointer”parse_and_validate_value() is the type-aware gate. For each vartype it
parses the textual value, enforces the declared min/max (with unit-aware
error text), and then invokes the per-variable check hook. The check hook
may reject the value, rewrite it into a canonical form, and allocate an
opaque extra blob (a pre-computed derived form of the setting) that travels
alongside the value to the assign hook:
// parse_and_validate_value (PGC_INT arm) — src/backend/utils/misc/guc.ccase PGC_INT:{ struct config_int *conf = (struct config_int *) record; const char *hintmsg;
if (!parse_int(value, &newval->intval, conf->gen.flags, &hintmsg)) { ereport(elevel, ( /* invalid value, with unit hint */ )); return false; } if (newval->intval < conf->min || newval->intval > conf->max) { /* ... "%d is outside the valid range ... (min .. max)" ... */ return false; } if (!call_int_check_hook(conf, &newval->intval, newextra, source, elevel)) return false;}break;The call_*_check_hook wrappers establish the GUC-error reporting channel
(GUC_check_errmsg_string et al., which a hook sets via GUC_check_errdetail)
and translate a hook’s false return into a proper ereport:
// call_bool_check_hook — src/backend/utils/misc/guc.cif (!conf->check_hook) return true; /* no hook: trivially valid */GUC_check_errcode_value = ERRCODE_INVALID_PARAMETER_VALUE;GUC_check_errmsg_string = NULL;/* ... reset the other GUC_check_* strings ... */if (!conf->check_hook(newval, extra, source)){ ereport(elevel, (errcode(GUC_check_errcode_value), GUC_check_errmsg_string ? errmsg_internal("%s", GUC_check_errmsg_string) : errmsg("invalid value for parameter \"%s\": %d", conf->gen.name, (int) *newval), /* ... errdetail / errhint from the hook ... */ )); FlushErrorState(); return false;}return true;The division of labor is deliberate: the check hook runs before commit and
must be side-effect-free (it may be called speculatively, e.g. when
ALTER ROLE ... SET validates a proposed value with source == PGC_S_TEST),
while the assign hook runs at commit and performs the real side effect —
recomputing a derived global, re-arming a timer, invalidating a cache. The
extra blob is how the expensive parse done in the check hook is handed to the
assign hook without recomputation.
The extra mechanism also solves a rollback subtlety. Because a SET inside
an aborted transaction must revert cleanly, the assign hook is required to be
replayable in reverse: when AtEOXact_GUC() restores a prior value, it
re-runs the assign hook with the prior value’s saved extra, so the side
effect is undone exactly as it was originally applied. This is why the assign
hook must never fail — all the things that can fail (parsing, range checks,
semantic validation) are forced into the check hook, which runs while failure
is still cheap. By the time *variable is overwritten and the assign hook
fires, the value is known-good and the operation is guaranteed to complete.
That invariant — check hooks may reject, assign hooks may not — is the
contract every GUC author must honor, and it is what makes the transactional
stack sound: popping a stack entry on abort can always re-apply its saved
state without any chance of a mid-rollback error.
Transactional stacking: SET LOCAL and rollback
Section titled “Transactional stacking: SET LOCAL and rollback”Before a value is overwritten, push_old_value() saves the prior value onto a
per-variable stack tagged with the current transaction nesting level
(GUCNestLevel). The stack entry’s state (GUC_SET, GUC_SET_LOCAL,
GUC_SAVE) records why it was pushed, which determines what happens at
transaction or subtransaction end:
// push_old_value — src/backend/utils/misc/guc.c (entry-merge arm)stack = gconf->stack;if (stack && stack->nest_level >= GUCNestLevel){ Assert(stack->nest_level == GUCNestLevel); switch (action) { case GUC_ACTION_SET: /* SET overrides any prior action at same nest level */ if (stack->state == GUC_SET_LOCAL) discard_stack_value(gconf, &stack->masked); stack->state = GUC_SET; break; case GUC_ACTION_LOCAL: if (stack->state == GUC_SET) { /* SET then SET LOCAL: remember SET's value as "masked" */ stack->masked_scontext = gconf->scontext; set_stack_value(gconf, &stack->masked); stack->state = GUC_SET_LOCAL; } break; case GUC_ACTION_SAVE: Assert(stack->state == GUC_SAVE); break; } return;}At transaction end, AtEOXact_GUC(isCommit, nestLevel) walks every variable
with a non-empty stack and pops entries at or above nestLevel, restoring the
prior value on abort (or on commit for SET LOCAL / GUC_SAVE entries) and
keeping it on commit for a plain GUC_SET. This is the mechanism behind
SET LOCAL (reverts at COMMIT), savepoint rollback (reverts the
subtransaction’s SETs), and function SET clauses (GUC_ACTION_SAVE, reverts
on function exit). The lifecycle of one variable’s value:
flowchart TD
A["boot_val<br/>source=PGC_S_DEFAULT"] --> B["startup: file/argv/env<br/>set reset_val too"]
B --> C["session running<br/>current value"]
C -->|"SET x = v<br/>GUC_ACTION_SET"| D["push_old_value(SET)<br/>then *variable=v"]
C -->|"SET LOCAL x = v<br/>GUC_ACTION_LOCAL"| E["push_old_value(LOCAL)<br/>masks current"]
D -->|"COMMIT"| F["AtEOXact_GUC: keep v"]
D -->|"ROLLBACK"| G["AtEOXact_GUC: restore prior"]
E -->|"COMMIT or ROLLBACK"| H["AtEOXact_GUC: restore prior<br/>(LOCAL never survives)"]
C -->|"RESET x"| I["*variable = reset_val<br/>source=reset_source"]
SET / SHOW and SIGHUP reload
Section titled “SET / SHOW and SIGHUP reload”The SQL surface lives in guc_funcs.c. ExecSetVariableStmt() dispatches on
the parsed VariableSetStmt: a plain SET x = v flattens its argument list
and calls set_config_option() with PGC_SUSET if the caller is a superuser
else PGC_USERSET; RESET and SET ... TO DEFAULT call it with a NULL value;
and SET TRANSACTION / SET SESSION CHARACTERISTICS fan out into several
underlying GUCs. SHOW reads back through GetPGVariable →
ShowGUCOption(), which applies any show_hook and unit formatting.
// ExecSetVariableStmt (VAR_SET_VALUE arm) — src/backend/utils/misc/guc_funcs.c(void) set_config_option(stmt->name, ExtractSetVariableArgs(stmt), (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION, action, true, 0, false);The grammar can hand SET a list of arguments (SET search_path = a, b, c),
so before the value reaches the funnel it is flattened to a single canonical
string by flatten_set_variable_args(), which consults the variable’s flags
to decide whether list input is even legal and whether each element must be
quoted:
// flatten_set_variable_args — src/backend/utils/misc/guc_funcs.crecord = find_option(name, false, true, WARNING);flags = record ? record->flags : 0;
/* Complain if list input and non-list variable */if ((flags & GUC_LIST_INPUT) == 0 && list_length(args) != 1) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("SET %s takes only one argument", name)));The GUC_LIST_INPUT / GUC_LIST_QUOTE flags are the declarative counterpart
to this logic: a single bit in the table tells the flattener how to assemble
the canonical string, so list-valued parameters such as search_path need no
special-case code beyond their flag.
postgresql.conf is applied by ProcessConfigFile() →
ProcessConfigFileInternal(), which parses the main file and
PG_AUTOCONF_FILENAME (postgresql.auto.conf, the file ALTER SYSTEM writes)
into a ConfigVariable list, then diffs it against current state and
re-applies via the same set_config_with_handle() funnel with
source = PGC_S_FILE. A PGC_POSTMASTER variable changed in the file is
detected here and reported as needing a restart rather than applied. Reload is
driven by SIGHUP: the signal handler sets the ConfigReloadPending flag, and
each process notices it in its main loop:
// the SIGHUP-reload idiom — e.g. src/backend/tcop/postgres.cif (ConfigReloadPending){ ConfigReloadPending = false; ProcessConfigFile(PGC_SIGHUP);}Because each backend independently calls ProcessConfigFile(PGC_SIGHUP), the
reload is decentralized: the postmaster, checkpointer, archiver, autovacuum,
WAL processes, and every regular backend each re-read and re-apply the file on
the same signal. (The exact per-process plumbing — how SIGHUP is delivered to
the whole process tree — is covered in postgres-postmaster.md.) The decentral
design means there is no single “configuration coordinator”: consistency comes
from every process running the same deterministic ProcessConfigFile over the
same files, so they all converge on the same effective values without any
inter-process handshake. A consequence worth noting is that a reload is not
atomic across the process tree — for a brief window after a SIGHUP, different
backends may observe different values for a just-changed PGC_SIGHUP
parameter, until each has reached the point in its loop where it checks
ConfigReloadPending. For the parameters that ride this path (timeouts,
logging levels, planner toggles) that transient skew is harmless.
Finally, parameters flagged GUC_REPORT (e.g. client_encoding,
DateStyle, application_name, in_hot_standby) are pushed to the client as
protocol-level ParameterStatus messages: a successful change sets the
GUC_NEEDS_REPORT status bit and links the variable onto guc_report_list;
ReportChangedGUCOptions() (run just before waiting for the next query) drains
the list and emits one message per changed variable, and
BeginReportingGUCOptions() sends the initial values at backend startup.
Source Walkthrough
Section titled “Source Walkthrough”The subsystem splits cleanly across three files plus two headers. Reading order, by call flow:
Record layout and enums (src/include/utils/guc_tables.h,
src/include/utils/guc.h). Start with config_generic — the shared header
whose name must be first so generic code can cast. Then the five typed
records config_bool / config_int / config_real / config_string /
config_enum, each embedding gen and adding variable, boot_val, the
three hook pointers, and (for numeric) min/max. enum config_group is the
pg_settings taxonomy. In guc.h: GucContext (the seven-rung
privilege/timing ladder), GucSource (value provenance), GucAction
(GUC_ACTION_SET / LOCAL / SAVE), the hook typedefs
(GucBoolCheckHook …), and the GUC_* flag bits (GUC_LIST_INPUT,
GUC_NO_SHOW_ALL, GUC_EXPLAIN, GUC_REPORT, GUC_UNIT_*, …).
The variable tables (src/backend/utils/misc/guc_tables.c). Five
NULL-terminated arrays: ConfigureNamesBool[], ConfigureNamesInt[],
ConfigureNamesReal[], ConfigureNamesString[], ConfigureNamesEnum[]. Each
entry is a nested brace-initializer. Enum variables additionally reference a
config_enum_entry[] options array (e.g. backslash_quote_options,
bytea_output_options). This file is the literal catalog of every core GUC.
Bootstrap and lookup (src/backend/utils/misc/guc.c).
build_guc_variables() counts the arrays, sets each vartype, and builds
guc_hashtab (a dynahash with custom guc_name_hash / guc_name_compare for
ASCII-only case-insensitive matching that is stable across setlocale).
find_option() resolves a name through the hash, maps obsolete aliases via
map_old_guc_names, and (when asked) mints placeholders via
add_placeholder_variable / assignable_custom_variable_name.
InitializeGUCOptions() / InitializeGUCOptionsFromEnvironment() seed every
variable from its boot_val and from environment variables at process start.
The write funnel (guc.c). set_config_option /
set_config_option_ext / set_config_with_handle is the single mutation
path. It enforces the record->context switch (the ladder), calls
parse_and_validate_value() (which calls parse_int/parse_real/parse_bool
plus the call_*_check_hook wrappers), invokes push_old_value() to stack the
prior value, assigns *variable, fires the assign hook, and (for GUC_REPORT)
sets GUC_NEEDS_REPORT. parse_and_validate_value() and the five
call_<type>_check_hook functions implement validation; push_old_value() and
AtEOXact_GUC() implement transactional scoping.
Read-back and reporting (guc.c). ShowGUCOption() renders a variable’s
current value (applying show_hook and unit conversion) for SHOW and
pg_settings. GetConfigOption() is the C-side reader.
BeginReportingGUCOptions() / ReportChangedGUCOptions() / ReportGUCOption
drive the ParameterStatus protocol traffic for GUC_REPORT variables.
Config-file processing (guc.c). ProcessConfigFileInternal() parses
postgresql.conf + postgresql.auto.conf into a ConfigVariable list and
re-applies the diff through the funnel; it is invoked via ProcessConfigFile()
on SIGHUP. The ordering is deliberate: the main file is parsed first, then
PG_AUTOCONF_FILENAME is parsed after it so that ALTER SYSTEM-written
values win over hand-edited postgresql.conf values for the same parameter.
A special case guards the bootstrap window before DataDir is known: until the
data directory is located, only a data_directory setting from the main file
is honored, because every other setting might be overridden by the not-yet-read
auto.conf. After applying, the function also resets any variable that was
previously set from the file but is now absent — so deleting a line from
postgresql.conf and reloading reverts that parameter to its non-file value,
not its last file value.
The parse machinery itself (ParseConfigFile / ParseConfigFp /
ParseConfigDirectory, declared in guc.h, implemented in guc-file.l)
handles include, include_dir, and include_if_exists directives and emits
a linked list of ConfigVariable nodes carrying name, value, filename,
and sourceline. The sourcefile/sourceline fields propagate into each
config_generic so pg_settings.sourcefile can tell a DBA which included
file set a parameter — invaluable when a value comes from a deeply nested
include_dir.
SQL surface (src/backend/utils/misc/guc_funcs.c).
ExecSetVariableStmt() implements SET / RESET / SET TRANSACTION;
flatten_set_variable_args() / ExtractSetVariableArgs() turn a parsed arg
list into the canonical string; SetPGVariable() is the helper entry;
GetPGVariable() / ShowAllGUCConfig() implement SHOW and SHOW ALL;
ProcessGUCArray() (in guc.c) applies the proconfig / setconfig arrays
attached to functions, roles, and databases.
Position hints (as of 2026-06-05, REL_18 273fe94)
Section titled “Position hints (as of 2026-06-05, REL_18 273fe94)”| Symbol | File | Line |
|---|---|---|
struct config_generic | src/include/utils/guc_tables.h | 171 |
struct config_bool | src/include/utils/guc_tables.h | 216 |
struct config_int | src/include/utils/guc_tables.h | 230 |
enum config_group | src/include/utils/guc_tables.h | 55 |
GucContext (enum end) | src/include/utils/guc.h | 80 |
GucSource (enum end) | src/include/utils/guc.h | 127 |
GucAction (enum end) | src/include/utils/guc.h | 206 |
GUC_REPORT flag | src/include/utils/guc.h | 220 |
ConfigureNamesBool[] | src/backend/utils/misc/guc_tables.c | 799 |
ConfigureNamesInt[] | src/backend/utils/misc/guc_tables.c | 2162 |
ConfigureNamesReal[] | src/backend/utils/misc/guc_tables.c | 3879 |
ConfigureNamesString[] | src/backend/utils/misc/guc_tables.c | 4170 |
ConfigureNamesEnum[] | src/backend/utils/misc/guc_tables.c | 5004 |
ProcessConfigFileInternal | src/backend/utils/misc/guc.c | 282 |
build_guc_variables | src/backend/utils/misc/guc.c | 903 |
add_guc_variable | src/backend/utils/misc/guc.c | 1047 |
find_option | src/backend/utils/misc/guc.c | 1235 |
guc_name_compare | src/backend/utils/misc/guc.c | 1300 |
guc_name_hash | src/backend/utils/misc/guc.c | 1330 |
InitializeGUCOptions | src/backend/utils/misc/guc.c | 1530 |
push_old_value | src/backend/utils/misc/guc.c | 2134 |
AtEOXact_GUC | src/backend/utils/misc/guc.c | 2262 |
BeginReportingGUCOptions | src/backend/utils/misc/guc.c | 2546 |
ReportChangedGUCOptions | src/backend/utils/misc/guc.c | 2596 |
parse_and_validate_value | src/backend/utils/misc/guc.c | 3129 |
set_config_with_handle | src/backend/utils/misc/guc.c | 3405 |
GetConfigOption | src/backend/utils/misc/guc.c | 4355 |
ShowGUCOption | src/backend/utils/misc/guc.c | 5471 |
call_bool_check_hook | src/backend/utils/misc/guc.c | 6810 |
ExecSetVariableStmt | src/backend/utils/misc/guc_funcs.c | 43 |
ExtractSetVariableArgs | src/backend/utils/misc/guc_funcs.c | 167 |
flatten_set_variable_args | src/backend/utils/misc/guc_funcs.c | 192 |
SetPGVariable | src/backend/utils/misc/guc_funcs.c | 315 |
GetPGVariable | src/backend/utils/misc/guc_funcs.c | 382 |
Source verification (as of 2026-06-05)
Section titled “Source verification (as of 2026-06-05)”Verified against /data/hgryoo/references/postgres at REL_18_STABLE, commit
273fe94 (PG 18.x). Checks performed:
-
Record layout.
struct config_genericbegins withconst char *nameannotated/* name of variable - MUST BE FIRST */, confirming the cast-to- header idiom. The five typed structs (config_bool/int/real/string/enum) each embedstruct config_generic genas their first member and carry thevariable/boot_val/check_hook/assign_hook/show_hookfields quoted above; numeric records addmin/max. Confirmed insrc/include/utils/guc_tables.h. -
Context ladder.
GucContextenumerates exactly the seven rungsPGC_INTERNAL, PGC_POSTMASTER, PGC_SIGHUP, PGC_SU_BACKEND, PGC_BACKEND, PGC_SUSET, PGC_USERSETinsrc/include/utils/guc.h. Theset_config_with_handle()switch (record->context)enforces each rung — including thePGC_SU_BACKEND→PGC_BACKENDfall-through withpg_parameter_aclcheck, thePGC_POSTMASTER-on-SIGHUPprohibitValueChangepath, and thePGC_SUSETACL check — exactly as described. -
Tables. All five
ConfigureNames*[]arrays exist inguc_tables.cat the lines tabulated. Theenable_seqscan(bool,PGC_USERSET,GUC_EXPLAIN) andmin_dynamic_shared_memory(int,PGC_POSTMASTER,GUC_UNIT_MB) entries are quoted verbatim. -
Funnel and hooks.
build_guc_variables()buildsguc_hashtabwithguc_name_hash/guc_name_match;find_option()resolves through it and creates placeholders.parse_and_validate_value()performs range checks and callscall_int_check_hooketc.;call_bool_check_hook()establishes theGUC_check_*reporting channel as quoted.push_old_value()/AtEOXact_GUC()implement the transactional stack. -
SQL + reload surface.
ExecSetVariableStmt()callsset_config_optionwithsuperuser() ? PGC_SUSET : PGC_USERSETandPGC_S_SESSIONforVAR_SET_VALUE, as quoted.ProcessConfigFileInternal()parses the main file plusPG_AUTOCONF_FILENAME. Theif (ConfigReloadPending) { ... ProcessConfigFile(PGC_SIGHUP); }idiom appears intcop/postgres.cand the parallel idiom inpostmaster/interrupt.c,checkpointer.c,pgarch.c,syslogger.c,startup.c, and others — confirming decentralized reload. -
Scope discipline. This doc deliberately does not assert PG19-only facts. All symbols, enum members, and flag bits cited resolve in the REL_18 tree.
contrib/is out of scope; custom-variable placeholders are described only via the in-corefind_option/add_placeholder_variablepath.
Beyond PostgreSQL — Comparative Designs & Research Frontiers
Section titled “Beyond PostgreSQL — Comparative Designs & Research Frontiers”MySQL / InnoDB. MySQL’s system variables share PostgreSQL’s
descriptor-array spirit (each variable is a sys_var object with a type, a
scope — GLOBAL / SESSION / both — and check/update functions), but the
interface is SQL-first: SET GLOBAL changes a running server’s global value
directly, and SET PERSIST (8.0+) writes it to mysqld-auto.cnf, the close
analog of PostgreSQL’s ALTER SYSTEM → postgresql.auto.conf. MySQL leans
harder on dynamic variables (more knobs are changeable without restart) at the
cost of a more complex global/session interaction; PostgreSQL’s PGC_* ladder
makes binding time a static, declarative property of each variable.
Oracle. Oracle’s initialization parameters carry an explicit SCOPE
(MEMORY / SPFILE / BOTH) on ALTER SYSTEM, separating “change the
running instance” from “persist for next start” — a distinction PostgreSQL
encodes implicitly through the PGC_POSTMASTER vs PGC_SIGHUP contexts plus
the auto.conf file. Oracle additionally exposes parameters as a queryable
V$PARAMETER view, mirroring PostgreSQL’s pg_settings.
SQLite. At the opposite pole, SQLite’s PRAGMA mechanism is per-connection
and largely transient, with no shared-memory or multi-process coordination to
worry about — a reminder that PostgreSQL’s machinery exists precisely because
its values must be coherent across a process tree and survive reload.
Self-tuning and research frontiers. Architecture of a Database System
(Hellerstein et al. 2007) already noted the tuning burden as a motivation for
the AutoAdmin line of work. The modern frontier is automatic configuration:
systems like OtterTune treat the GUC surface as a high-dimensional optimization
problem and use machine learning to search it, while “self-driving” database
proposals (Pavlo et al.) fold knob tuning into a closed control loop. What
makes PostgreSQL a friendly target for these is exactly the design described
here — a uniform, introspectable catalog (pg_settings) with declared types,
ranges, units, and binding times — so an external tuner can enumerate the
search space, respect which knobs need a restart, and apply changes through
ALTER SYSTEM without bespoke per-parameter glue. The check/assign-hook split
is also what lets a knob carry semantic validation a generic tuner can rely on
rather than discovering invalid combinations only at runtime.
A second frontier is per-tenant / per-workload configuration in
multi-tenant deployments: PostgreSQL already supports per-database and per-role
defaults (ALTER DATABASE/ROLE ... SET, applied via ProcessGUCArray with
PGC_S_DATABASE / PGC_S_USER sources), which is a coarse form of the
workload-aware configuration that cloud database services elaborate into
managed parameter groups. The provenance ordering described earlier is what
makes this layering well-defined: a per-database default outranks the file but
is outranked by an interactive SET, so a session can still tune itself above
the tenant baseline. Pushing this further — per-query or per-operator
configuration chosen by a learned policy — is an active research direction, and
the GUC_ACTION_SAVE / function-SET-clause machinery (the same path that
backs CREATE FUNCTION ... SET work_mem) is already the natural insertion
point for scoped, auto-reverting overrides.
A third practical theme is observability of configuration itself. Because
every value carries its source, scontext, srole, sourcefile, and
sourceline, pg_settings and pg_file_settings let an operator answer “why
is this parameter set to this value, and who set it?” without guesswork — a
capability that the descriptor-array design provides almost for free, since the
provenance fields are just more columns on the same record. Engines built on a
flat key-value store typically bolt this on after the fact, if at all. The
lesson the GUC subsystem teaches is that making binding time, authority,
and provenance first-class fields of a single uniform record — rather than
conventions scattered across call sites — is what lets one mechanism scale to a
thousand knobs while staying introspectable, tunable, and safe.
Sources
Section titled “Sources”- Code (REL_18_STABLE, commit 273fe94, PG 18.x):
src/backend/utils/misc/guc.c— bootstrap (build_guc_variables), lookup (find_option), the write funnel (set_config_with_handle), validation (parse_and_validate_value,call_*_check_hook), transactional stacking (push_old_value,AtEOXact_GUC), read-back (ShowGUCOption,GetConfigOption), reporting (BeginReportingGUCOptions,ReportChangedGUCOptions), and config-file processing (ProcessConfigFileInternal).src/backend/utils/misc/guc_tables.c— the fiveConfigureNames*[]variable arrays and the enum-option tables.src/backend/utils/misc/guc_funcs.c— the SQL surface (ExecSetVariableStmt,SetPGVariable,GetPGVariable,flatten_set_variable_args).src/include/utils/guc.h,src/include/utils/guc_tables.h— theconfig_genericheader, the five typed records, and theGucContext/GucSource/GucAction/config_groupenums andGUC_*flag bits.
- Theory:
- Hellerstein, Stonebraker & Hamilton, Architecture of a Database System
(2007) —
knowledge/research/dbms-papers/fntdb07-architecture.md(DBMS component decomposition and the tuning burden). - Cross-references:
knowledge/research/dbms-papers/README.mdand the paper-bibliography map in.omc/plans/postgres-paper-bibliography.md.
- Hellerstein, Stonebraker & Hamilton, Architecture of a Database System
(2007) —
- Sibling docs (cross-reference, not duplicated here):
postgres-backend-lifecycle.md— backend GUC inheritance and parallel-worker GUC state copy (RestoreGUCState).postgres-postmaster.md— SIGHUP delivery across the process tree.postgres-overview-base-infra.md— where the GUC subsystem sits among the base-infrastructure modules.