PostgreSQL uses operating system file descriptors to access database files, WAL files, sockets, shared memory objects, configuration files, and dynamically loaded extensions. Every client connection is served by a dedicated backend process, and each backend opens the resources it needs while executing queries.
To prevent excessive resource consumption, postgres provides the max_files_per_process configuration parameter, which defines the maximum number of files that each server process can keep open simultaneously.
When applications such as Odoo maintain multiple database connections, understanding how backend processes use file descriptors becomes valuable for troubleshooting resource limits and verifying Postgresql configuration changes.
Linux provides several utilities that make it possible to inspect these backend processes and examine the files they have opened.
Check the current value of this parameter.
show max_files_per_process ;
Result :
max_files_per_process
-----------------------
1000
(1 row)
We can get more details of this parameter from the pg_settings catalogue.
select * from pg_settings where name = 'max_files_per_process';
Result :
-[ RECORD 1 ]---+------------------------------------------------------------------------------
name | max_files_per_process
setting | 1000
unit |
category | Resource Usage / Kernel Resources
short_desc | Sets the maximum number of simultaneously open files for each server process.
extra_desc |
context | postmaster
vartype | integer
source | default
min_val | 64
max_val | 2147483647
enumvals |
boot_val | 1000
reset_val | 1000
sourcefile |
sourceline |
pending_restart | f
This is the guc parameter definition from the guc_parameters.dat file from the postgres source code version 19.
{ name => 'max_files_per_process', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_KERNEL',
short_desc => 'Sets the maximum number of files each server process is allowed to open simultaneously.',
variable => 'max_files_per_process',
boot_val => '1000',
min => '64',
max => 'INT_MAX',
},We are running the Odoo application on Postgres, so our postgres has already some database connections.
Check the current backend connection status of the database from the pg_stat_activity.
select pid,usename,application_name,wait_event,wait_event_type,state from pg_stat_activity where application_name ilike '%odoo%' ;
Result :
pid | usename | application_name | wait_event | wait_event_type | state --------+----------+------------------+------------+-----------------+------- 338126 | cybrosys | odoo-337989 | ClientRead | Client | idle 338122 | cybrosys | odoo-337989 | ClientRead | Client | idle 338121 | cybrosys | odoo-337989 | ClientRead | Client | idle 338120 | cybrosys | odoo-337989 | ClientRead | Client | idle 338118 | cybrosys | odoo-337989 | ClientRead | Client | idle 338117 | cybrosys | odoo-337989 | ClientRead | Client | idle 338116 | cybrosys | odoo-337989 | ClientRead | Client | idle 338115 | cybrosys | odoo-337989 | ClientRead | Client | idle 338114 | cybrosys | odoo-337989 | ClientRead | Client | idle 338113 | cybrosys | odoo-337989 | ClientRead | Client | idle 338066 | cybrosys | odoo-337989 | ClientRead | Client | idle 338048 | cybrosys | odoo-337989 | ClientRead | Client | idle 338047 | cybrosys | odoo-337989 | ClientRead | Client | idle 338005 | cybrosys | odoo-337989 | ClientRead | Client | idle 337999 | cybrosys | odoo-337989 | ClientRead | Client | idle 337996 | cybrosys | odoo-337989 | ClientRead | Client | idle 337995 | cybrosys | odoo-337989 | ClientRead | Client | idle
In Linux, we have a command name lsof - list open files
Check the available options based on the lsof command.
lsof --help
Result:
lsof: illegal option character: -
lsof: -e not followed by a file system path: "lp"
lsof 4.93.2
latest revision: https://github.com/lsof-org/lsof
latest FAQ: https://github.com/lsof-org/lsof/blob/master/00FAQ
latest (non-formatted) man page: https://github.com/lsof-org/lsof/blob/master/Lsof.8
usage: [-?abhKlnNoOPRtUvVX] [+|-c c] [+|-d s] [+D D] [+|-E] [+|-e s] [+|-f[gG]]
[-F [f]] [-g [s]] [-i [i]] [+|-L [l]] [+m [m]] [+|-M] [-o [o]] [-p s]
[+|-r [t]] [-s [p:s]] [-S [t]] [-T [t]] [-u s] [+|-w] [-x [fl]] [--] [names]
Defaults in parentheses; comma-separated set (s) items; dash-separated ranges.
-?|-h list help -a AND selections (OR) -b avoid kernel blocks
-c c cmd c ^c /c/[bix] +c w COMMAND width (9) +d s dir s files
-d s select by FD set +D D dir D tree *SLOW?* +|-e s exempt s *RISKY*
-i select IPv[46] files -K [i] list|(i)gn tasKs -l list UID numbers
-n no host names -N select NFS files -o list file offset
-O no overhead *RISKY* -P no port names -R list paRent PID
-s list file size -t terse listing -T disable TCP/TPI info
-U select Unix socket -v list version info -V verbose search
+|-w Warnings (+) -X skip TCP&UDP* files -Z Z context [Z]
-- end option scan
-E display endpoint info +E display endpoint info and files
+f|-f +filesystem or -file names +|-f[gG] flaGs
-F [f] select fields; -F? for help
+|-L [l] list (+) suppress (-) link counts < l (0 = all; default = 0)
+m [m] use|create mount supplement
+|-M portMap registration (-) -o o o 0t offset digits (8)
-p s exclude(^)|select PIDs -S [t] t second stat timeout (15)
-T qs TCP/TPI Q,St (s) info
-g [s] exclude(^)|select and print process group IDs
-i i select by IPv[46] address: [46][proto][@host|addr][:svc_list|port_list]
+|-r [t[m<fmt>]] repeat every t seconds (15); + until no files, - forever.
An optional suffix to t is m<fmt>; m must separate t from <fmt> and
<fmt> is an strftime(3) format for the marker line.
-s p:s exclude(^)|select protocol (p = TCP|UDP) states by name(s).
-u s exclude(^)|select login|UID set s
-x [fl] cross over +d|+D File systems or symbolic Links
names select named files or files on named file systems
Anyone can list all files; /dev warnings disabled; kernel ID check disabled.
Now take one of the process IDs based on the database connection and use that process ID with the lsof command like this.
Now we use the process ID with the lsof command to list all the files and resources opened by the process with PID (Process ID) 338126.
sudo lsof -p 338126
Result:
lsof: WARNING: can't stat() fuse.portal file system /run/user/1000/doc Output information may be incomplete.lsof: WARNING: can't stat() fuse.gvfsd-fuse file system /run/user/1000/gvfs Output information may be incomplete.COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAMEpostgres 338126 postgres cwd DIR 259,3 4096 56492036 /var/lib/postgresql/18/mainpostgres 338126 postgres rtd DIR 259,3 4096 2 /postgres 338126 postgres txt REG 259,3 11475488 21370598 /usr/lib/postgresql/18/bin/postgrespostgres 338126 postgres mem REG 0,27 2097152 122719 /dev/shm/PostgreSQL.3353442048postgres 338126 postgres mem REG 0,27 1048576 122718 /dev/shm/PostgreSQL.3561796722postgres 338126 postgres DEL REG 0,1 12374 /dev/zeropostgres 338126 postgres mem REG 259,3 1082600 21365828 /usr/lib/postgresql/18/lib/pg_vault_tde.sopostgres 338126 postgres mem REG 259,3 1000120 20741579 /usr/local/lib/libcurl.so.4.8.0postgres 338126 postgres mem REG 259,3 487200 21365849 /usr/lib/postgresql/18/lib/pg_tde.sopostgres 338126 postgres mem REG 259,3 5712208 20581489 /usr/lib/locale/locale-archivepostgres 338126 postgres mem REG 259,3 137560 20606208 /usr/lib/x86_64-linux-gnu/libbrotlicommon.so.1.0.9postgres 338126 postgres mem REG 259,3 75768 20607135 /usr/lib/x86_64-linux-gnu/libpsl.so.5.3.2postgres 338126 postgres mem REG 259,3 526896 20606578 /usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1postgres 338126 postgres mem REG 259,3 1743016 20607385 /usr/lib/x86_64-linux-gnu/libunistring.so.2.2.0
We can also use the wc -l option to count the total number of open file entries for the process with PID 338126.
sudo lsof -p 338126 | wc -l
Result:
lsof: WARNING: can't stat() fuse.portal file system /run/user/1000/doc
Output information may be incomplete.
lsof: WARNING: can't stat() fuse.gvfsd-fuse file system /run/user/1000/gvfs
Output information may be incomplete.
117
Now here we can see the count of files opened by this process is 117.
Every PostgreSQL server is controlled by a single parent process known as the postmaster. This process starts the database server, accepts new client connections, launches background worker processes, and creates a dedicated backend process for every client connection.
To identify the parent process of the PostgreSQL 18 cluster, execute the following command:
ps -ef | grep "/var/lib/postgresql/18/main"
Result :
postgres 337816 1 0 20:15 ? 00:00:00 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main -c config_file=/etc/postgresql/18/main/postgresql.conf
postgres 337817 1 0 20:15 ? 00:00:00 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main2 -c config_file=/etc/postgresql/18/main2/postgresql.conf
cybrosys 339115 338262 0 20:22 pts/9 00:00:00 grep --color=auto /var/lib/postgresql/18/main
The process ID of the postmaster of postgres version 18 cluster is 337816.
Once the postmaster process has been identified, we can display all of the backend processes that belong to this PostgreSQL cluster.
pstree -ap 337816
Result :
postgres,337816 -D /var/lib/postgresql/18/main -c config_file=/etc/postgresql/18/main/postgresql.conf
+-postgres,337833
+-postgres,337834
+-postgres,337835
+-postgres,337836
+-postgres,337837
+-postgres,337843
+-postgres,337844
+-postgres,337845
+-postgres,337969
+-postgres,337995
+-postgres,337996
+-postgres,337999
+-postgres,338005
+-postgres,338047
+-postgres,338048
+-postgres,338066
+-postgres,338113
+-postgres,338114
+-postgres,338115
+-postgres,338116
+-postgres,338117
+-postgres,338118
+-postgres,338120
+-postgres,338121
+-postgres,338122
+-postgres,338126
The process tree clearly illustrates how Postgres manages its server processes. The postmaster process appears at the top, while each child process performs a specific task.
Some child processes are background workers responsible for maintenance activities such as checkpoints, WAL writing, autovacuum, and background writing.
Other child processes are backend processes created for active client connections, including those established by Odoo.
This hierarchy makes it easy to identify every process that belongs to the Postgres cluster.
Check File Descriptor Usage for Every Backend Process
Since the max_files_per_process parameter applies to every Postgres server process, examining the file descriptor usage of all backend processes provides a better understanding of how Postgres consumes operating system resources.
The following shell script iterates through the postmaster process and all of its child processes, counts the number of open file descriptors for each process, and displays the results.
sudo bash -c '
for pid in 337816 $(pgrep -P 337816); do
printf "%-8s %3d\n" "$pid" "(ls/proc/pid/fd | wc -l)"
done
'
Result :
337816 10
337833 166
337834 20
337835 14
337836 7
337837 6
337843 7
337844 7
337845 7
337969 53
337995 14
337996 9
337999 373
338005 535
338047 295
338048 414
338066 9
338113 104
338114 341
338115 23
338116 104
338117 337
338118 76
338120 71
338121 23
338122 23
338126 57
Now we can see each backend process ID and its file usage count also.
Now change the value of this parameter to 100 and reload the Postgres configuration using the select pg_reload_conf() like this.
alter system set max_files_per_process = '100';
select pg_reload_conf();
Result :
pg_reload_conf
----------------
t
(1 row)
Now check the value.
show max_files_per_process ;
Result :
max_files_per_process
-----------------------
1000
(1 row)
The changed value is not reflected because of the parameter context, so we need to restart the postgres.
sudo systemctl restart postgresql
Now check the value again.
show max_files_per_process ;
Result:
max_files_per_process
-----------------------
100
(1 row)
Now check the process ID of the postmaster of postgres version 18.
ps -ef | grep "postgres -D /var/lib/postgresql/18/main"
Result :
postgres 339506 1 0 20:24 ? 00:00:00 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main -c config_file=/etc/postgresql/18/main/postgresql.conf
postgres 339507 1 0 20:24 ? 00:00:00 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main2 -c config_file=/etc/postgresql/18/main2/postgresql.conf
cybrosys 339722 338262 0 20:25 pts/9 00:00:00 grep --color=auto postgres -D /var/lib/postgresql/18/main
The process ID of postmaster postgres version 18 is 339506.
Now use the shell command below to check each process ID and its file usage count also.
sudo bash -c '
for pid in 339506 $(pgrep -P 339506); do
printf "%-8s %3d\n" "$pid" "(ls/proc/pid/fd | wc -l)"
done
'
Result :
337816 0
339518 50
339520 15
339522 7
339524 6
339526 6
339533 7
339534 7
339535 7
339666 9
Before vs. After Comparison
Before restarting PostgreSQL, the server was operating with a max_files_per_process value of 1000. Several backend processes had hundreds of open file descriptors depending on the workload generated by Odoo.
After changing the configuration and restarting PostgreSQL, the parameter value became 100, and the server created a new set of backend processes.
Although the configured limit changed, the actual number of open file descriptors still depends on the workload assigned to each backend. PostgreSQL opens only the files required for current operations and does not automatically allocate file descriptors up to the configured limit.
This experiment demonstrates that max_files_per_process specifies the upper limit available to each process rather than the number of files that every backend will always open.
The max_files_per_process parameter determines the maximum number of files that each PostgreSQL server process can open simultaneously. By combining PostgreSQL system views with Linux process inspection tools, it is possible to observe how backend processes consume operating system file descriptors and understand the relationship between PostgreSQL configuration and system resources.