Skip to content

PostgreSQL GUC Parameters — config_generic, PGC Contexts, SET/SHOW, Check/Assign Hooks, and SIGHUP Reload

Contents:

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:

  1. 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 = 4MB also carries a unit that must be normalized to a canonical base unit.

  2. 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 fsync is not something an ordinary user should do). Some are per-session and freely settable by anyone (work_mem within a query). A configuration system needs a lattice of “contexts” that says, for each parameter, the earliest binding time and the minimum privilege.

  3. 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).

  4. Scoping and rollback. An interactive change made inside a transaction that later aborts must revert. A SET LOCAL must revert at transaction end. A function with a SET clause 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.

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 read work_mem on 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’s SET GLOBAL + config, Oracle’s ALTER 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.

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.h
struct 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.h
struct 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.

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.h
typedef 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.c
switch (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.

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.c
hentry = (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"]

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.c
case 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.c
if (!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"]

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 GetPGVariableShowGUCOption(), 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.c
record = 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.c
if (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.

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)”
SymbolFileLine
struct config_genericsrc/include/utils/guc_tables.h171
struct config_boolsrc/include/utils/guc_tables.h216
struct config_intsrc/include/utils/guc_tables.h230
enum config_groupsrc/include/utils/guc_tables.h55
GucContext (enum end)src/include/utils/guc.h80
GucSource (enum end)src/include/utils/guc.h127
GucAction (enum end)src/include/utils/guc.h206
GUC_REPORT flagsrc/include/utils/guc.h220
ConfigureNamesBool[]src/backend/utils/misc/guc_tables.c799
ConfigureNamesInt[]src/backend/utils/misc/guc_tables.c2162
ConfigureNamesReal[]src/backend/utils/misc/guc_tables.c3879
ConfigureNamesString[]src/backend/utils/misc/guc_tables.c4170
ConfigureNamesEnum[]src/backend/utils/misc/guc_tables.c5004
ProcessConfigFileInternalsrc/backend/utils/misc/guc.c282
build_guc_variablessrc/backend/utils/misc/guc.c903
add_guc_variablesrc/backend/utils/misc/guc.c1047
find_optionsrc/backend/utils/misc/guc.c1235
guc_name_comparesrc/backend/utils/misc/guc.c1300
guc_name_hashsrc/backend/utils/misc/guc.c1330
InitializeGUCOptionssrc/backend/utils/misc/guc.c1530
push_old_valuesrc/backend/utils/misc/guc.c2134
AtEOXact_GUCsrc/backend/utils/misc/guc.c2262
BeginReportingGUCOptionssrc/backend/utils/misc/guc.c2546
ReportChangedGUCOptionssrc/backend/utils/misc/guc.c2596
parse_and_validate_valuesrc/backend/utils/misc/guc.c3129
set_config_with_handlesrc/backend/utils/misc/guc.c3405
GetConfigOptionsrc/backend/utils/misc/guc.c4355
ShowGUCOptionsrc/backend/utils/misc/guc.c5471
call_bool_check_hooksrc/backend/utils/misc/guc.c6810
ExecSetVariableStmtsrc/backend/utils/misc/guc_funcs.c43
ExtractSetVariableArgssrc/backend/utils/misc/guc_funcs.c167
flatten_set_variable_argssrc/backend/utils/misc/guc_funcs.c192
SetPGVariablesrc/backend/utils/misc/guc_funcs.c315
GetPGVariablesrc/backend/utils/misc/guc_funcs.c382

Verified against /data/hgryoo/references/postgres at REL_18_STABLE, commit 273fe94 (PG 18.x). Checks performed:

  • Record layout. struct config_generic begins with const char *name annotated /* name of variable - MUST BE FIRST */, confirming the cast-to- header idiom. The five typed structs (config_bool/int/real/string/enum) each embed struct config_generic gen as their first member and carry the variable / boot_val / check_hook / assign_hook / show_hook fields quoted above; numeric records add min/max. Confirmed in src/include/utils/guc_tables.h.

  • Context ladder. GucContext enumerates exactly the seven rungs PGC_INTERNAL, PGC_POSTMASTER, PGC_SIGHUP, PGC_SU_BACKEND, PGC_BACKEND, PGC_SUSET, PGC_USERSET in src/include/utils/guc.h. The set_config_with_handle() switch (record->context) enforces each rung — including the PGC_SU_BACKENDPGC_BACKEND fall-through with pg_parameter_aclcheck, the PGC_POSTMASTER-on-SIGHUP prohibitValueChange path, and the PGC_SUSET ACL check — exactly as described.

  • Tables. All five ConfigureNames*[] arrays exist in guc_tables.c at the lines tabulated. The enable_seqscan (bool, PGC_USERSET, GUC_EXPLAIN) and min_dynamic_shared_memory (int, PGC_POSTMASTER, GUC_UNIT_MB) entries are quoted verbatim.

  • Funnel and hooks. build_guc_variables() builds guc_hashtab with guc_name_hash / guc_name_match; find_option() resolves through it and creates placeholders. parse_and_validate_value() performs range checks and calls call_int_check_hook etc.; call_bool_check_hook() establishes the GUC_check_* reporting channel as quoted. push_old_value() / AtEOXact_GUC() implement the transactional stack.

  • SQL + reload surface. ExecSetVariableStmt() calls set_config_option with superuser() ? PGC_SUSET : PGC_USERSET and PGC_S_SESSION for VAR_SET_VALUE, as quoted. ProcessConfigFileInternal() parses the main file plus PG_AUTOCONF_FILENAME. The if (ConfigReloadPending) { ... ProcessConfigFile(PGC_SIGHUP); } idiom appears in tcop/postgres.c and the parallel idiom in postmaster/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-core find_option / add_placeholder_variable path.

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 SYSTEMpostgresql.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.

  • 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 five ConfigureNames*[] 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 — the config_generic header, the five typed records, and the GucContext / GucSource / GucAction / config_group enums and GUC_* 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.md and the paper-bibliography map in .omc/plans/postgres-paper-bibliography.md.
  • 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.