Filesystem Quotas

Disk quotas are per-identity limits on how much of a filesystem a user, group, or project may consume — both in blocks (disk space) and in inodes (number of files). The kernel enforces two limits per resource: a hard limit that can never be crossed, and a softer soft limit that may be exceeded only for a bounded grace period, after which the soft limit becomes binding (kernel quota.rst). Linux has two quota implementations under one quotactl(2) syscall: a generic VFS quota layer (dquot, used by ext4, ext3, ext2, F2FS, and others) that journals quota records into hidden filesystem inodes, and XFS’s native quota manager (XQM), which treats quota data as ordinary journaled metadata and is driven by a parallel set of Q_X* operations. This note covers the limit model, the on-disk quota files/inodes, the dquot charging machinery, the userspace tools (quotaon, quotactl, repquota, edquota), and project quotas — the directory-tree quota mechanism that XFS and ext4 share. It cross-links ext4 Filesystem Internals and XFS Filesystem Internals, which own those filesystems’ on-disk formats; this note owns the quota subsystem itself.

The Limit Model — soft, hard, grace

For each combination of identity (a specific UID, GID, or project ID) and resource (space or inodes) a quota record holds a hard limit and a soft limit. The semantics, verbatim from the kernel quota documentation: “A user can never exceed a hardlimit for any resource (unless he has CAP_SYS_RESOURCE capability). User is allowed to exceed softlimit but only for limited period of time. This period is called ‘grace period’ or ‘grace time’. When grace time is over, user is not able to allocate more space/inodes until he frees enough of them to get below softlimit.”

So the soft limit is a warning threshold with a deadline. The first time a user crosses it, a timer (one week by default per quotactl(2)) starts; while the timer runs the user may keep allocating up to the hard limit, but once it expires the soft limit hardens — further allocation fails with EDQUOT until the user deletes enough to drop back below soft. Grace times are set per-filesystem, separately for blocks and inodes (dqi_bgrace/dqi_igrace in struct dqinfo). The distinction between the two resources matters: a user can be space-rich but inode-poor (many tiny files) or the reverse, so each is limited and timed independently.

flowchart LR
  A["usage below<br/>soft limit"] -->|allocate past soft| B["over soft,<br/>grace timer running"]
  B -->|free below soft| A
  B -->|grace expires| C["soft now binding<br/>→ EDQUOT"]
  B -->|reach hard| D["hard limit<br/>→ EDQUOT"]
  C -->|free below soft| A
  D -->|free below hard| B

The quota state machine for one identity/resource pair. What it shows: the soft limit is permeable only while the grace timer runs; crossing the hard limit, or letting grace expire, both produce EDQUOT on the next allocation. The insight: quotas fail allocations, not the process — a write that would exceed quota returns EDQUOT to that syscall, leaving already-written data intact; the limit is checked at block/inode allocation time, which is exactly where the dquot layer hooks in.

Two Implementations, One Syscall

Everything userspace does to quotas goes through quotactl(2):

int quotactl(int cmd, const char *special, int id, caddr_t addr);

cmd is built with the macro QCMD(subcmd, type), where type is USRQUOTA, GRPQUOTA, or (since Linux 4.1) PRJQUOTA, and subcmd selects the operation. special is the path to the mounted block device, id is the UID/GID/project ID, and addr points to a command-specific struct (quotactl(2)). The interface is bifurcated:

  • Generic VFS operations (Q_QUOTAON, Q_QUOTAOFF, Q_GETQUOTA, Q_SETQUOTA, Q_GETINFO, Q_SETINFO, Q_GETFMT, Q_GETNEXTQUOTA, Q_SYNC) drive the dquot layer used by ext4/ext3/F2FS/etc. They exchange struct dqblk (limits + current usage) and struct dqinfo (grace times + flags).
  • XFS operations (Q_XQUOTAON, Q_XQUOTAOFF, Q_XGETQUOTA, Q_XSETQLIM, Q_XGETQSTAT, Q_XGETQSTATV, Q_XQUOTARM, Q_XGETNEXTQUOTA) bypass the generic layer and talk to XQM. They exchange struct fs_disk_quota and struct fs_quota_stat(v), and they understand a third resource XFS supports — realtime blocks (d_rtb_*) — that the generic layer lacks.

The struct dqblk exchanged by Q_GETQUOTA/Q_SETQUOTA makes the model concrete (from quotactl(2)):

struct dqblk {
    uint64_t dqb_bhardlimit;  /* absolute limit on quota blocks */
    uint64_t dqb_bsoftlimit;  /* preferred (soft) block limit   */
    uint64_t dqb_curspace;    /* current occupied space (bytes) */
    uint64_t dqb_ihardlimit;  /* max number of inodes           */
    uint64_t dqb_isoftlimit;  /* preferred (soft) inode limit   */
    uint64_t dqb_curinodes;   /* current allocated inodes       */
    uint64_t dqb_btime;       /* grace-expiry time for blocks   */
    uint64_t dqb_itime;       /* grace-expiry time for inodes   */
    uint32_t dqb_valid;       /* QIF_* mask of which fields set */
};

dqb_valid is the key to Q_SETQUOTA: you can set just the block limits (QIF_BLIMITS) or just the grace times (QIF_BTIME|QIF_ITIME) by flagging only those fields, leaving the rest untouched — this is how edquota edits one dimension without clobbering the others.

The fd-based variant — quotactl_fd

A modern problem with quotactl(2) is that special must be a block device path. That excludes filesystems without a backing block device and is awkward in containers. Linux 5.14 (2021) added quotactl_fd(2) (syscall 443), which takes an open file descriptor — possibly opened with O_PATH — referring to any file or directory on the target filesystem instead of a device path (quotactl_fd(2)). It “avoids this limitation and is simpler to use.” This is the current, container-friendly entry point; the cmd/id/addr semantics are otherwise identical to quotactl.

Where Quota Data Lives — files vs hidden inodes vs metadata

There are three storage strategies, and which one a filesystem uses determines how quotas are administered.

  1. Visible quota files (legacy / old formats). The original Linux quota stored records in regular files at the filesystem root: aquota.user and aquota.group (formerly quota.user/quota.group). With these, Q_QUOTAON’s addr argument is the pathname of that file, and you must run quotacheck(8) to build/repair the file by scanning every inode before turning quotas on. Three on-disk formats exist, selected by the id passed to Q_QUOTAON: QFMT_VFS_OLD (original), QFMT_VFS_V0 (32-bit IDs, limits to 2⁴² bytes / 2³² inodes), and QFMT_VFS_V1 (limits to 2⁶⁴ bytes / 2⁶⁴ inodes) (quotactl(2)).

  2. Hidden system inodes (modern ext4/F2FS). Modern ext4 stores quota records in hidden reserved inodes that have no directory entry — there is no visible aquota.* file. As the quotactl(2) man page states: “Quota information can be also stored in hidden system inodes for ext4, XFS, and other filesystems if the filesystem is configured so. In this case, there are no visible quota files and there is no need to use quotacheck(8). Quota information is always kept consistent by the filesystem … and the Q_QUOTAON operation serves only to enable enforcement.” The presence of hidden inodes is reported by the DQF_SYS_FILE flag (bit 1<<16) in the dqi_flags field from Q_GETINFO. On ext4 this is understood to be a superblock quota feature (enabled at filesystem-creation time so the hidden inodes exist — see the uncertainty note below on the exact e2fsprogs invocation), distinct from the usrquota/grpquota mount options — which, per the ext4 admin guide, “are ignored by the filesystem [and] are used only by quota tools to recognize volumes where quota should be turned on.”

  3. First-class journaled metadata (XFS). XFS does not use the VFS dquot files at all. Per the xfs_quota(8) documentation, “XFS considers quota information as filesystem metadata and uses journaling to provide a higher level guarantee of consistency.” Quota records (dquots) live in internal metadata inodes managed by XQM, are recovered by log replay like any other XFS metadata, and never need an offline quotacheck.

A subtlety for journaled VFS quotas: because quota updates must be crash-consistent on ext4 with a journal, the filesystem must know which inodes hold quota data so it can replay them. That is the purpose of the usrjquota=/grpjquota= + jqfmt= mount options the ext4 admin guide documents — they “inform the filesystem about quota details so that quota information updates correctly during journal replay.” This connects quotas to The jbd2 Journaling Layer.

The VFS dquot Layer — how charges are tracked

For the generic implementation, the in-memory quota object is struct dquot, holding one identity’s usage and limits, cached and reference-counted. The filesystem hooks the VFS operations so that every allocation passes through the dquot accounting in fs/quota/dquot.c (v6.12). The mechanical flow:

  • dquot_initialize(inode) runs the first time an inode needs charging. It looks up (via dqget) the dquots for the inode’s owning UID, GID, and — if project quotas are on — project ID, and pins pointers to them on the inode. An inode flagged S_NOQUOTA (the quota files themselves) is skipped, so quota accounting never recurses into quota storage.
  • __dquot_alloc_space(inode, number, flags) is called by the filesystem before allocating number bytes. It walks the inode’s dquots, checks each against soft/hard limits, and either charges them (incrementing dqb_curspace) or returns -EDQUOT. The inode-allocation path has a parallel dquot_alloc_inode. Frees call the corresponding dquot_free_*.
  • dquot_transfer(inode, ...) runs on chown/chgrp: it moves the inode’s accumulated space and inode counts from the old owner’s dquots to the new owner’s, atomically, so a change of ownership re-bills the right identity. The kernel comment notes this routine had to be carefully made race-free against concurrent allocation (dquot.c head comment).

These are exposed to filesystems as the dquot_operations table (dquot.c, v6.12), which a filesystem’s super_block->dq_op points at. The locking is intricate — the head comment of dquot.c enumerates five SMP locks (dq_list_lock, dquot->dq_dqb_lock, inode->i_lock, dq_data_lock, dq_state_lock) with a strict ordering, and uses an SRCU (dquot_srcu) so that reading the inode→dquot pointers is lockless on the hot path and freeing a dquot waits for readers to drain. The design lineage, per the same comment, is the BSD/Melbourne quota system — the quotactl “BSD system call interface” is the userspace contract.

Project Quotas — quotas on a directory tree

User and group quotas bill by ownership, but sometimes you want to cap a directory tree regardless of who owns the files in it — e.g. “this project directory may use at most 10 GiB.” That is what project quotas provide, available on both XFS and ext4. A project is just a numeric project ID; files are tagged with one, and the project’s dquot accumulates the usage of every file carrying that ID.

The mechanism is inheritance. You mark a directory with a project ID and set an inherit flag so that new files and subdirectories created underneath automatically receive the same project ID:

  • On XFS, the xfs_quota(8) docs describe it directly: “XFS supports the notion of project quota, which can be used to implement a form of directory tree quota (i.e. to restrict a directory tree to only being able to use up a component of the filesystem’s available space…).” You seed the tree with xfs_quota’s project -s subcommand, which “marks inodes with project identifiers”; thereafter “new files created in the tree will automatically be accounted to the tree based on their project identifier.” The inherit flag is FS_XFLAG_PROJINHERIT on the directory inode.
  • On ext4, the same model is provided by the project feature plus the prjquota mount option; the project ID and the inherit flag are inode attributes (chattr -p <id> sets the project ID, chattr +P sets project inheritance on a directory).

Uncertain

The exact ext4 chattr flags for project ID (-p) and project inheritance (+P) and the prjquota mount-option spelling are from general e2fsprogs/chattr(1) knowledge, not confirmed against a primary kernel/e2fsprogs doc fetched for this note — the ext4 admin guide I consulted lists only quota,noquota,grpquota,usrquota and does not document prjquota or the project feature. Verify against the chattr(1) man page and e2fsprogs release notes. The XFS side (project -s, FS_XFLAG_PROJINHERIT) is confirmed by xfs_quota(8). uncertain

Project quotas are heavily used in practice: they are how shared filesystems give each tenant a directory-tree budget without per-user accounting, and how container/build systems cap a workspace.

Userspace Tools

The kernel exposes only quotactl/quotactl_fd; the operator-facing tools are the quota-tools package (linuxquota project):

  • quotaon(8) / quotaoff(8) issue Q_QUOTAON/Q_QUOTAOFF (or the XFS Q_XQUOTAON) to enable/disable enforcement. With hidden inodes or XFS metadata, quotaon only enables enforcement; the accounting was always live.
  • quotacheck(8) scans every inode to build or repair visible aquota.* files. It is not needed with hidden-inode or XFS quotas, where the filesystem keeps counts consistent itself.
  • edquota(8) opens an editor on a user/group/project’s limits, translating the edits into a Q_SETQUOTA with the right dqb_valid mask.
  • setquota(8) sets limits non-interactively (scriptable edquota).
  • repquota(8) prints a usage/limit summary for a filesystem. Per repquota(8), it shows “current number of files and disk space … quota limits … [and] two-character markers indicating exceeded limits”; -a reports all quota’d filesystems from /etc/mtab, -u/-g/-P select user/group/project, and -s prints human-readable units.
  • xfs_quota(8) is XFS’s dedicated tool, with limit, report, and project subcommands, since XFS quotas use the Q_X* interface rather than the generic one.

A typical setup on ext4 with hidden inodes is, in outline: enable the quota feature at filesystem-creation time (so the hidden quota inodes exist), mount with usrquota,grpquota, run quotaon, then set limits with setquota/edquota. On XFS: mount with uquota (accounting is then on for the life of that mount), then set limits with xfs_quota -x -c 'limit bsoft=5g bhard=6g alice' /home.

Uncertain

The precise commands for the ext4 setup — the mkfs.ext4 -O quota / tune2fs -O quota invocation to enable the quota feature, and setquota’s positional argument units (block limits are traditionally in raw KiB, so a literal 5G may not be accepted) — are from general e2fsprogs/quota-tools knowledge and were not verified against mke2fs(8), tune2fs(8), or setquota(8) for this note. The ext4 admin guide I consulted does not document the quota feature flag at all (only the mount options). The XFS xfs_quota limit syntax above follows xfs_quota(8). To resolve: check mke2fs(8)/tune2fs(8) for -O quota and setquota(8) for unit handling. uncertain

Failure Modes and Common Misunderstandings

  • EDQUOT, not ENOSPC. A quota-limited write fails with EDQUOT; a genuinely full filesystem fails with ENOSPC. Conflating them sends you debugging the wrong thing.
  • Mount options vs feature flags (ext4). Adding usrquota to /etc/fstab does nothing by itself on a modern ext4 unless the quota feature is enabled — the mount option is only a hint to quota-tools. People expect the option to “turn on quotas”; with hidden inodes it does not create the storage.
  • XFS accounting cannot be toggled at runtime for the root filesystem. XFS turns quota accounting on at mount time; per xfs_quota(8), “Quota accounting must be turned on at the time of mounting the XFS filesystem.” For the root filesystem you generally must add the mount option and reboot/remount; you can toggle enforcement separately but not enable accounting on a tree that was mounted without it.
  • Root and CAP_SYS_RESOURCE bypass hard limits. A process with CAP_SYS_RESOURCE (typically root) can exceed even the hard limit — quotas are not a security boundary against a privileged process.
  • Grace times are per-filesystem, set once. Editing one user’s grace timer is not how you change the policy; grace periods are a Q_SETINFO property of the whole filesystem (dqi_bgrace/dqi_igrace), distinct from a user’s current grace-expiry timestamp in dqb_btime.
  • The kernel must be built with CONFIG_QUOTA. Otherwise quotactl returns -ENOSYS (quotactl(2)).

Alternatives and When To Choose Them

  • Quotas vs RLIMIT_FSIZE/cgroups. A getrlimit/setrlimit RLIMIT_FSIZE caps the size of a single file a process may create, not aggregate usage by identity — a different axis. For total-bytes-per-container budgeting, the modern alternative is often a dedicated filesystem/loopback image or a CoW subvolume quota rather than POSIX quotas. Project quotas remain the best fit when many identities share one filesystem and you need per-tree budgets.
  • Btrfs qgroups. Btrfs has its own quota-group (qgroup) system tied to subvolumes and snapshots, not the quotactl interface — a separate mechanism covered with Btrfs Copy-on-Write Subvolumes and Snapshots, chosen implicitly when you choose Btrfs.
  • VFS dquot vs XFS XQM. You do not usually choose between them — the filesystem does. ext4/F2FS give you the generic layer with visible-file or hidden-inode storage; XFS gives you native journaled quotas and realtime-block accounting via the Q_X* ops.

Production Notes

Project quotas are the production workhorse: shared HPC and NFS-exported filesystems give each group a directory-tree budget via XFS or ext4 project IDs, and container/CI systems use them to cap scratch workspaces because they bill a tree regardless of the (often root-mapped) UID inside the container. The XFS choice of treating quotas as journaled metadata — no quotacheck, crash-consistent by log replay — is a major operational advantage at scale and a reason large fileservers favor XFS. The quota netlink interface (VFS_DQUOT generic-netlink family, QUOTA_NL_C_WARNING) lets a userspace daemon surface “you are over quota” notifications to graphical desktops, since the kernel’s traditional terminal warnings are invisible there (quota.rst); note that interface is only delivered in the initial network namespace, a limitation for containerized monitoring.

See Also